Skip to content

Commit

Permalink
Added SearchBuilder & searchables
Browse files Browse the repository at this point in the history
  • Loading branch information
jzaplet committed May 3, 2024
1 parent 7087e83 commit 8eed94f
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 53 deletions.
1 change: 1 addition & 0 deletions config/app.neon
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
- Megio\Collection\RecipeFinder
- Megio\Collection\ReadBuilder\ReadBuilder
- Megio\Collection\WriteBuilder\WriteBuilder
- Megio\Collection\SearchBuilder\SearchBuilder

- Megio\Security\JWT\JWTResolver
- Megio\Security\JWT\ClaimsFormatter
Expand Down
14 changes: 10 additions & 4 deletions src/Collection/CollectionRecipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,38 @@

use Doctrine\ORM\Mapping\Table;
use Megio\Collection\Exception\CollectionException;
use Megio\Collection\SearchBuilder\SearchBuilder;
use Megio\Collection\WriteBuilder\WriteBuilder;
use Megio\Collection\ReadBuilder\ReadBuilder;
use Megio\Database\Interface\ICrudable;

abstract class CollectionRecipe implements ICollectionRecipe
{
public function read(ReadBuilder $builder, RecipeRequest $request): ReadBuilder
public function read(ReadBuilder $builder, CollectionRequest $request): ReadBuilder
{
return $builder->buildByDbSchema();
}

public function readAll(ReadBuilder $builder, RecipeRequest $request): ReadBuilder
public function readAll(ReadBuilder $builder, CollectionRequest $request): ReadBuilder
{
return $builder->buildByDbSchema();
}

public function create(WriteBuilder $builder, RecipeRequest $request): WriteBuilder
public function create(WriteBuilder $builder, CollectionRequest $request): WriteBuilder
{
return $builder->buildByDbSchema();
}

public function update(WriteBuilder $builder, RecipeRequest $request): WriteBuilder
public function update(WriteBuilder $builder, CollectionRequest $request): WriteBuilder
{
return $builder->buildByDbSchema();
}

public function search(SearchBuilder $builder, CollectionRequest $request): SearchBuilder
{
return $builder->keepDefaults();
}

/**
* @throws \Megio\Collection\Exception\CollectionException
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@

namespace Megio\Collection;

use Megio\Collection\WriteBuilder\Field\Base\UndefinedValue;
use Symfony\Component\HttpFoundation\Request;

class RecipeRequest
class CollectionRequest
{
/**
* @param \Symfony\Component\HttpFoundation\Request $request
* @param bool $isFormRendering
* @param array<int|string, mixed> $requestData
* @param string|null $rowId
* @param array<string, string|int|float|bool|null> $rowValues
* @param mixed|UndefinedValue $customData
*/
public function __construct(
protected Request &$request,
protected bool $isFormRendering,
protected mixed $requestData,
protected ?string $rowId = null,
protected array $rowValues = [],
protected mixed $customData = new UndefinedValue(),
)
{
}
Expand All @@ -45,8 +44,29 @@ public function getRowValues(): mixed
return $this->rowValues;
}

/**
* @return array<int|string, mixed>
*/
public function getRequestData(): mixed
{
return $this->requestData;
}

public function getCustomData(): mixed
{
return $this->customData;
if (array_key_exists('custom_data', $this->requestData)) {
return $this->requestData['custom_data'];
}

return [];
}

public function getSearchText(): ?string
{
if (array_key_exists('search', $this->requestData) && array_key_exists('text', $this->requestData['search'])) {
return $this->requestData['search']['text'];
}

return null;
}
}
12 changes: 7 additions & 5 deletions src/Collection/ICollectionRecipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace Megio\Collection;

use Megio\Collection\SearchBuilder\SearchBuilder;
use Megio\Collection\WriteBuilder\WriteBuilder;
use Megio\Collection\ReadBuilder\ReadBuilder;
use Symfony\Component\HttpFoundation\Request;

interface ICollectionRecipe
{
Expand All @@ -16,14 +16,16 @@ public function source(): string;
public function key(): string;

/** @throws \Megio\Collection\Exception\CollectionException */
public function read(ReadBuilder $builder, RecipeRequest $request): ReadBuilder;
public function read(ReadBuilder $builder, CollectionRequest $request): ReadBuilder;

/** @throws \Megio\Collection\Exception\CollectionException */
public function readAll(ReadBuilder $builder, RecipeRequest $request): ReadBuilder;
public function readAll(ReadBuilder $builder, CollectionRequest $request): ReadBuilder;

public function create(WriteBuilder $builder, RecipeRequest $request): WriteBuilder;
public function create(WriteBuilder $builder, CollectionRequest $request): WriteBuilder;

public function update(WriteBuilder $builder, RecipeRequest $request): WriteBuilder;
public function update(WriteBuilder $builder, CollectionRequest $request): WriteBuilder;

public function search(SearchBuilder $builder, CollectionRequest $request): SearchBuilder;

/** @throws \Megio\Collection\Exception\CollectionException */
public function getEntityMetadata(): RecipeEntityMetadata;
Expand Down
106 changes: 106 additions & 0 deletions src/Collection/SearchBuilder/SearchBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);

namespace Megio\Collection\SearchBuilder;

use Doctrine\ORM\QueryBuilder;
use Megio\Collection\CollectionRequest;

class SearchBuilder
{
protected QueryBuilder $queryBuilder;
protected CollectionRequest $request;

/** @var array<string, Searchable> */
protected array $searchables = [];

public function create(QueryBuilder $qb, CollectionRequest $request): self
{
$this->queryBuilder = $qb;
$this->request = $request;

return $this;
}

public function build(): QueryBuilder
{
// Text search
$searchText = $this->request->getSearchText();

if ($searchText !== null) {
$whereDql = [];

foreach ($this->searchables as $searchable) {
$relationCol = $searchable->getRelation();
$colName = 'entity.' . $searchable->getColumn();

if ($relationCol !== null) {
$alias = 'alias_' . $searchable->getRelation() . '_' . $searchable->getColumn();
$this->queryBuilder->leftJoin("entity.{$relationCol}", $alias);
$colName = "{$alias}.{$searchable->getColumn()}";
}

$paramName = 'param_' . str_replace('.', '_', $colName);
$value = $searchable->hasFormatter() ? $searchable->format($searchText) : "%{$searchText}%";

$whereDql[] = [
'dql' => "{$colName} {$searchable->getOperator()} :{$paramName}",
'paramName' => $paramName,
'paramValue' => $value
];
}

if (count($whereDql) !== 0) {
$where = implode(' OR ', array_map(fn($where) => $where['dql'], $whereDql));
$this->queryBuilder->andWhere($where);

foreach ($whereDql as $where) {
$this->queryBuilder->setParameter($where['paramName'], $where['paramValue']);
}
}
}

return $this->queryBuilder;
}

/**
* @param string[] $searchables
*/
public function keepDefaults(array $searchables = ['id', 'createdAt', 'updatedAt']): self
{
foreach ($searchables as $columnName) {
$this->addSearchable(new Searchable($columnName));
}

return $this;
}

public function addSearchable(Searchable $searchable): self
{
$this->searchables[$searchable->getColumn()] = $searchable;
return $this;
}

public function getQueryBuilder(): QueryBuilder
{
return $this->queryBuilder;
}

public function getRequest(): CollectionRequest
{
return $this->request;
}

/** @return array{
* searchables: array{column: string, relation: string|null, operator: string}[]
* }
*/
public function toArray(): array
{
$searchables = array_map(fn(Searchable $searchable) => $searchable->toArray(), $this->searchables);

return [
'searchables' => array_values($searchables),
];
}
}
59 changes: 59 additions & 0 deletions src/Collection/SearchBuilder/Searchable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);

namespace Megio\Collection\SearchBuilder;

class Searchable
{
/** @var array<int, callable> */
private array $formatter = [];

public function __construct(
protected string $column,
protected ?string $relation = null,
protected string $operator = 'LIKE',
?callable $formatter = null
)
{
if ($formatter !== null) {
$this->formatter[] = $formatter;
}
}

public function getColumn(): string
{
return $this->column;
}

public function getRelation(): ?string
{
return $this->relation;
}

public function getOperator(): string
{
return $this->operator;
}

public function format(?string $value): mixed
{
return $this->formatter[0]($value);
}

public function hasFormatter(): bool
{
return count($this->formatter) !== 0;
}

/**
* @return array{column: string, relation: string|null, operator: string}
*/
public function toArray(): array
{
return [
'column' => $this->column,
'relation' => $this->relation,
'operator' => $this->operator,
];
}
}
1 change: 0 additions & 1 deletion src/Event/Collection/OnFormStartEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
namespace Megio\Event\Collection;

use Megio\Collection\ICollectionRecipe;
use Megio\Collection\RecipeRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\Event;
Expand Down
6 changes: 3 additions & 3 deletions src/Http/Request/Collection/CreateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
namespace Megio\Http\Request\Collection;

use Doctrine\DBAL\Exception\ConstraintViolationException;
use Megio\Collection\CollectionRequest;
use Megio\Collection\Exception\SerializerException;
use Megio\Collection\RecipeRequest;
use Megio\Collection\WriteBuilder\WriteBuilder;
use Megio\Collection\WriteBuilder\WriteBuilderEvent;
use Megio\Collection\Exception\CollectionException;
Expand Down Expand Up @@ -67,10 +67,10 @@ public function process(array $data): Response
$ids = [];

foreach ($data['rows'] as $row) {
$recipeRequest = new RecipeRequest($this->request, false, null, $row, $data['custom_data']);
$collectionRequest = new CollectionRequest($this->request, false, $data, null, $row);

try {
$builder = $recipe->create($this->builder->create($recipe, WriteBuilderEvent::CREATE, null, $row), $recipeRequest)->build();
$builder = $recipe->create($this->builder->create($recipe, WriteBuilderEvent::CREATE, null, $row), $collectionRequest)->build();
} catch (CollectionException $e) {
$response = $this->error([$e->getMessage()], 406);
$event = new OnExceptionEvent(EventType::CREATE, $data, $recipe, $e, $this->request, $response);
Expand Down
6 changes: 3 additions & 3 deletions src/Http/Request/Collection/Form/CreatingFormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

namespace Megio\Http\Request\Collection\Form;

use Megio\Collection\CollectionRequest;
use Megio\Collection\Exception\CollectionException;
use Megio\Collection\RecipeRequest;
use Megio\Collection\WriteBuilder\WriteBuilder;
use Megio\Collection\WriteBuilder\WriteBuilderEvent;
use Megio\Collection\RecipeFinder;
Expand Down Expand Up @@ -48,10 +48,10 @@ public function process(array $data): Response
return $dispatcher->getResponse();
}

$recipeRequest = new RecipeRequest($this->request, true, null, [], $data['custom_data']);
$collectionRequest = new CollectionRequest($this->request, true, $data, null, []);

try {
$builder = $recipe->create($this->builder->create($recipe, WriteBuilderEvent::CREATE), $recipeRequest)->build();
$builder = $recipe->create($this->builder->create($recipe, WriteBuilderEvent::CREATE), $collectionRequest)->build();
} catch (CollectionException $e) {
return $this->error([$e->getMessage()]);
}
Expand Down
Loading

0 comments on commit 8eed94f

Please sign in to comment.