-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CollectionRecipes for describing CRUD behaviour
- Loading branch information
Showing
32 changed files
with
636 additions
and
437 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
namespace Megio\Collection; | ||
|
||
enum CollectionPropType | ||
{ | ||
case INVISIBLE; | ||
case SHOW_ONE; | ||
case SHOW_ALL; | ||
case NONE; | ||
|
||
/** | ||
* @param \Megio\Collection\ICollectionRecipe $recipe | ||
* @return string[] | ||
*/ | ||
public function getPropNames(ICollectionRecipe $recipe): array | ||
{ | ||
return match ($this) { | ||
self::INVISIBLE => array_merge(['id'], $recipe->invisibleColumns()), | ||
self::SHOW_ONE => array_merge(['id'], $recipe->showOneColumns()), | ||
self::SHOW_ALL => array_merge(['id'], $recipe->showAllColumns()), | ||
self::NONE => [], | ||
}; | ||
} | ||
|
||
/** | ||
* @param array{maxLength: int|null, name: string, nullable: bool, type: string}[] $schema | ||
* @param \Megio\Collection\ICollectionRecipe $recipe | ||
* @return array{maxLength: int|null, name: string, nullable: bool, type: string}[] $schema | ||
*/ | ||
public function getAllowedPropNames(array $schema, ICollectionRecipe $recipe): array | ||
{ | ||
$propNames = $this->getPropNames($recipe); | ||
$props = array_filter($schema, fn($field) => in_array($field['name'], $propNames)); | ||
|
||
return array_values($props); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Megio\Collection; | ||
|
||
use Doctrine\ORM\Mapping\Table; | ||
use Megio\Database\Interface\ICrudable; | ||
|
||
abstract class CollectionRecipe implements ICollectionRecipe | ||
{ | ||
public function invisibleColumns(): array | ||
{ | ||
return ['id', 'updatedAt']; | ||
} | ||
|
||
/** | ||
* @param \Megio\Collection\CollectionPropType $type | ||
* @return \Megio\Collection\RecipeEntityMetadata | ||
* @throws \Megio\Collection\CollectionException | ||
* @throws \ReflectionException | ||
*/ | ||
public final function getEntityMetadata(CollectionPropType $type): RecipeEntityMetadata | ||
{ | ||
if (!is_subclass_of($this->source(), ICrudable::class)) { | ||
throw new CollectionException("Entity '{$this->source()}' does not implement ICrudable"); | ||
} | ||
|
||
$rf = new \ReflectionClass($this->source()); | ||
$attr = $rf->getAttributes(Table::class); | ||
|
||
if (count($attr) === 0) { | ||
throw new CollectionException("Entity '{$this->source()}' is missing Table attribute"); | ||
} | ||
|
||
/** @var Table $attrInstance */ | ||
$attrInstance = $attr[0]->newInstance(); | ||
|
||
if ($attrInstance->name === null) { | ||
throw new CollectionException("Entity '{$this->source()}' has Table attribute without name"); | ||
} | ||
|
||
$tableName = str_replace('`', '', $attrInstance->name); | ||
$metadata = new RecipeEntityMetadata($this, $rf, $type, $tableName); | ||
|
||
if (count($metadata->getSchema()['props']) === 1) { | ||
throw new CollectionException("Collection '{$this->name()}' has no visible columns"); | ||
} | ||
|
||
return $metadata; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Megio\Collection; | ||
|
||
interface ICollectionRecipe | ||
{ | ||
/** @return class-string */ | ||
public function source(): string; | ||
|
||
/** @return string */ | ||
public function name(): string; | ||
|
||
/** @return string[] */ | ||
public function invisibleColumns(): array; | ||
|
||
/** @return string[] */ | ||
public function showOneColumns(): array; | ||
|
||
/** @return string[] */ | ||
public function showAllColumns(): array; | ||
|
||
/** | ||
* @throws \Megio\Collection\CollectionException | ||
* @throws \ReflectionException | ||
*/ | ||
public function getEntityMetadata(CollectionPropType $type): RecipeEntityMetadata; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Megio\Collection\Mapping; | ||
|
||
use Megio\Collection\CollectionException; | ||
use Megio\Collection\ICollectionRecipe; | ||
use Megio\Collection\RecipeEntityMetadata; | ||
use Megio\Database\Interface\ICrudable; | ||
|
||
class ArrayToEntity | ||
{ | ||
/** | ||
* @param \Megio\Collection\ICollectionRecipe $recipe | ||
* @param \Megio\Collection\RecipeEntityMetadata $metadata | ||
* @param array<string, int|float|string|bool> $data | ||
* @return \Megio\Database\Interface\ICrudable | ||
* @throws \Megio\Collection\CollectionException | ||
*/ | ||
public static function create(ICollectionRecipe $recipe, RecipeEntityMetadata $metadata, array $data): ICrudable | ||
{ | ||
$className = $recipe->source(); | ||
|
||
/** @var \Megio\Database\Interface\ICrudable $entity */ | ||
$entity = new $className(); | ||
|
||
return self::update($metadata, $entity, $data); | ||
} | ||
|
||
/** | ||
* @param \Megio\Collection\RecipeEntityMetadata $metadata | ||
* @param \Megio\Database\Interface\ICrudable $entity | ||
* @param array<string, int|float|string|bool> $data | ||
* @return \Megio\Database\Interface\ICrudable | ||
* @throws \Megio\Collection\CollectionException | ||
*/ | ||
public static function update(RecipeEntityMetadata $metadata, ICrudable $entity, array $data): ICrudable | ||
{ | ||
$ref = $metadata->getReflection(); | ||
$methods = array_map(fn($method) => $method->name, $ref->getMethods()); | ||
|
||
foreach ($data as $key => $value) { | ||
try { | ||
$methodName = 'set' . ucfirst($key); | ||
if (in_array($methodName, $methods)) { | ||
$m = $ref->getMethod($methodName)->name; | ||
$entity->$m($value); | ||
} else { | ||
$ref->getProperty($key)->setValue($entity, $value); | ||
} | ||
} catch (\ReflectionException) { | ||
throw new CollectionException("Property '{$key}' does not exist"); | ||
} | ||
} | ||
|
||
return $entity; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Megio\Collection; | ||
|
||
use Doctrine\ORM\Mapping\Column; | ||
|
||
readonly class RecipeEntityMetadata | ||
{ | ||
/** | ||
* @param \Megio\Collection\ICollectionRecipe $recipe | ||
* @param \ReflectionClass<\Megio\Database\Interface\ICrudable> $entityRef | ||
* @param \Megio\Collection\CollectionPropType $type | ||
* @param string $tableName | ||
*/ | ||
public function __construct( | ||
private ICollectionRecipe $recipe, | ||
private \ReflectionClass $entityRef, | ||
private CollectionPropType $type, | ||
private string $tableName, | ||
) | ||
{ | ||
} | ||
|
||
/** | ||
* @return string | ||
*/ | ||
public function getTableName(): string | ||
{ | ||
return $this->tableName; | ||
} | ||
|
||
/** | ||
* @return \ReflectionClass<\Megio\Database\Interface\ICrudable> | ||
*/ | ||
public function getReflection(): \ReflectionClass | ||
{ | ||
return $this->entityRef; | ||
} | ||
|
||
public function getQbSelect(string $qbAlias): string | ||
{ | ||
$schema = $this->getFullSchemaReflectedByDoctrine(); | ||
$visibleProps = $this->type->getAllowedPropNames($schema, $this->recipe); | ||
return implode(', ', array_map(fn($col) => $qbAlias . '.' . $col['name'], $visibleProps)); | ||
} | ||
|
||
/** | ||
* @return array{ | ||
* meta: array{table: string, invisible: string[]}, | ||
* props: array{maxLength: int|null, name: string, nullable: bool, type: string}[] | ||
* } | ||
*/ | ||
public function getSchema(): array | ||
{ | ||
$schema = $this->getFullSchemaReflectedByDoctrine(); | ||
$props = $this->type->getAllowedPropNames($schema, $this->recipe); | ||
|
||
return [ | ||
'meta' => [ | ||
'table' => $this->tableName, | ||
'invisible' => $this->recipe->invisibleColumns() | ||
], | ||
'props' => $this->sortColumns($props) | ||
]; | ||
} | ||
|
||
/** | ||
* @return array{maxLength: int|null, name: string, nullable: bool, type: string}[] | ||
*/ | ||
private function getFullSchemaReflectedByDoctrine(): array | ||
{ | ||
$props = []; | ||
foreach ($this->entityRef->getProperties() as $prop) { | ||
$attrs = array_map(fn($attr) => $attr->newInstance(), $prop->getAttributes()); | ||
|
||
/** @var Column[] $columnAttrs */ | ||
$columnAttrs = array_filter($attrs, fn($attr) => $attr instanceof Column); | ||
if (count($columnAttrs) !== 0) { | ||
$attr = array_values($columnAttrs)[0]; | ||
$props[] = $this->getColumnMetadata($attr, $prop); | ||
} | ||
} | ||
|
||
// move array item with name "id" to first position | ||
$idProp = array_filter($props, fn($prop) => $prop['name'] !== 'id'); | ||
return array_merge(array_values(array_filter($props, fn($prop) => $prop['name'] === 'id')), $idProp); | ||
} | ||
|
||
/** | ||
* @param \Doctrine\ORM\Mapping\Column $attr | ||
* @param \ReflectionProperty $prop | ||
* @return array{maxLength: int|null, name: string, nullable: bool, type: string} | ||
*/ | ||
private function getColumnMetadata(Column $attr, \ReflectionProperty $prop): array | ||
{ | ||
$propType = $prop->getType(); | ||
$nullable = $attr->nullable; | ||
|
||
$type = $attr->type; | ||
if ($type === null) { | ||
$type = $propType instanceof \ReflectionNamedType ? $propType->getName() : $propType ?? '@unknown'; | ||
} | ||
|
||
$maxLength = $attr->length; | ||
if ($maxLength === null && $type === 'string') { | ||
$maxLength = 255; | ||
} | ||
|
||
return [ | ||
'name' => $prop->getName(), | ||
'type' => mb_strtolower($type), | ||
'nullable' => $nullable, | ||
'maxLength' => $maxLength | ||
]; | ||
} | ||
|
||
|
||
/** | ||
* @param array{maxLength: int|null, name: string, nullable: bool, type: string}[] $fields | ||
* @return array{maxLength: int|null, name: string, nullable: bool, type: string}[] | ||
*/ | ||
private function sortColumns(array $fields): array | ||
{ | ||
$associativeFields = []; | ||
foreach ($fields as $field) { | ||
$associativeFields[$field['name']] = $field; | ||
} | ||
|
||
$sortedFields = []; | ||
foreach ($this->type->getPropNames($this->recipe) as $name) { | ||
if (array_key_exists($name, $associativeFields)) { | ||
$sortedFields[] = $associativeFields[$name]; | ||
} | ||
} | ||
|
||
return $sortedFields; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace Megio\Collection; | ||
|
||
use Megio\Helper\Path; | ||
use Nette\Utils\Finder; | ||
|
||
class RecipeFinder | ||
{ | ||
/** @var ICollectionRecipe[] */ | ||
protected array $recipes = []; | ||
|
||
public function load(): self | ||
{ | ||
$appFiles = Finder::findFiles()->from(Path::appDir() . '/Recipe'); | ||
foreach ($appFiles as $file) { | ||
$class = 'App\\Recipe\\' . $file->getBasename('.php'); | ||
if (is_subclass_of($class, ICollectionRecipe::class)) { | ||
$this->recipes[] = new $class(); | ||
} | ||
} | ||
|
||
$vendorFiles = Finder::findFiles()->from(Path::megioVendorDir() . '/src/Recipe'); | ||
foreach ($vendorFiles as $file) { | ||
$class = 'Megio\\Recipe\\' . $file->getBasename('.php'); | ||
if (is_subclass_of($class, ICollectionRecipe::class)) { | ||
$this->recipes[] = new $class(); | ||
} | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** @return ICollectionRecipe[] */ | ||
public function getAll(): array | ||
{ | ||
return $this->recipes; | ||
} | ||
|
||
public function findByName(string $name): ?ICollectionRecipe | ||
{ | ||
$recipe = current(array_filter($this->recipes, fn($r) => $r->name() === $name)); | ||
return $recipe ?: null; | ||
} | ||
} |
Oops, something went wrong.