Initialize Nest model library.
This commit is contained in:
commit
4f1ac0c9b4
35 changed files with 3272 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
vendor/
|
47
composer.json
Normal file
47
composer.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"version": "dev-main",
|
||||||
|
"name": "nest/model",
|
||||||
|
"description": "Nest model service.",
|
||||||
|
"type": "library",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Madeorsk",
|
||||||
|
"email": "madeorsk@protonmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Nest\\Model\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://code.zeptotech.net/Nest/Core"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://code.zeptotech.net/Nest/Events"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://code.zeptotech.net/Nest/Types"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://code.zeptotech.net/Nest/Database"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vcs",
|
||||||
|
"url": "https://code.zeptotech.net/Nest/Cli"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"nest/core": "dev-main",
|
||||||
|
"nest/events": "dev-main",
|
||||||
|
"nest/types": "dev-main",
|
||||||
|
"nest/database": "dev-main"
|
||||||
|
}
|
||||||
|
}
|
1037
composer.lock
generated
Normal file
1037
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
46
src/ArrayBlueprint.php
Normal file
46
src/ArrayBlueprint.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
abstract class ArrayBlueprint
|
||||||
|
{
|
||||||
|
protected string $type;
|
||||||
|
|
||||||
|
protected ?string $table = null;
|
||||||
|
|
||||||
|
protected ?string $foreignKeyName = null;
|
||||||
|
|
||||||
|
protected ?string $foreignValueName = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string $entityClass Entity class where the field is defined.
|
||||||
|
* @param string $name Name of the field.
|
||||||
|
*/
|
||||||
|
public function __construct(protected string $entityClass, protected string $name)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function type(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(string $table): static
|
||||||
|
{
|
||||||
|
$this->table = $table;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function foreignKeyName(string $foreignKeyName): static
|
||||||
|
{
|
||||||
|
$this->foreignValueName = $foreignKeyName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function foreignValueName(string $foreignValueName): static
|
||||||
|
{
|
||||||
|
$this->foreignValueName = $foreignValueName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
120
src/ArrayInstance.php
Normal file
120
src/ArrayInstance.php
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Database\Query\DeleteQuery;
|
||||||
|
use Nest\Database\Query\InsertQuery;
|
||||||
|
use Nest\Database\Query\Raw;
|
||||||
|
use Nest\Database\Query\SelectQuery;
|
||||||
|
use Nest\Database\Exceptions\NotCurrentTransactionException;
|
||||||
|
use Nest\Database\Exceptions\Query\MissingConditionValueException;
|
||||||
|
use Nest\Database\Exceptions\UnknownDatabaseException;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
use function Nest\Utils\array_first_or_val;
|
||||||
|
use function Nest\Utils\str_snake_singularize;
|
||||||
|
|
||||||
|
class ArrayInstance extends ReadableArrayBlueprint
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an array field instance from an array field definition.
|
||||||
|
* @param ArrayBlueprint $array
|
||||||
|
*/
|
||||||
|
public function __construct(ArrayBlueprint $array)
|
||||||
|
{
|
||||||
|
parent::__construct($array->entityClass, $array->name);
|
||||||
|
|
||||||
|
// Copy all fields.
|
||||||
|
$this->type = $array->type;
|
||||||
|
$this->table = $array->table;
|
||||||
|
$this->foreignKeyName = $array->foreignKeyName;
|
||||||
|
$this->foreignValueName = $array->foreignValueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current table / foreign key / foreign value state.
|
||||||
|
* @param string $fromTable Table from which to get values.
|
||||||
|
* @return array{string, string, string} Table, Foreign key, Foreign value.
|
||||||
|
*/
|
||||||
|
public function getState(string $fromTable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$table = $this->getTable() ?? "{$fromTable}_{$this->name}",
|
||||||
|
$foreignKey = $this->getForeignKeyName() ?? "$table.".str_snake_singularize($fromTable)."_id",
|
||||||
|
$foreignValue = $this->getForeignValueName() ?? "$table.".str_snake_singularize($this->name),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a column name from a fully qualified name.
|
||||||
|
* @return string The column name.
|
||||||
|
*/
|
||||||
|
private function getColumnName(string $fullyQualifiedName): string
|
||||||
|
{
|
||||||
|
return substr($fullyQualifiedName, strrpos($fullyQualifiedName, ".") + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup inline loading for the current array field in an entity query.
|
||||||
|
* @param EntityQuery $baseQuery The entity query to alter.
|
||||||
|
* @param string $prefix Prefix to add to the name of the properties.
|
||||||
|
* @return Raw Generated select subquery.
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
*/
|
||||||
|
public function genSelect(EntityQuery $baseQuery, string $prefix = ""): Raw
|
||||||
|
{
|
||||||
|
// Get all auto values for undefined tables or keys.
|
||||||
|
[$table, $foreignKey, $foreignValue] = $this->getState($baseQuery->getTableName());
|
||||||
|
|
||||||
|
// Build subquery SELECT SQL.
|
||||||
|
$sql = ((new SelectQuery($baseQuery->getDatabase(), $table))
|
||||||
|
->select($foreignValue)
|
||||||
|
->whereColumn("{$baseQuery->getTableName()}.".$baseQuery->getPrimaryKeyName(), "=", $foreignKey))->build();
|
||||||
|
|
||||||
|
// Return the raw SELECT of a JSON array.
|
||||||
|
return new Raw(
|
||||||
|
"ARRAY_TO_JSON(ARRAY($sql->sql)) AS $prefix$this->name",
|
||||||
|
$sql->bindings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Entity $entity Entity for which to save.
|
||||||
|
* @param array $value Array to save.
|
||||||
|
* @return void
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws NotCurrentTransactionException
|
||||||
|
*/
|
||||||
|
public function save(Entity $entity, array $value): void
|
||||||
|
{
|
||||||
|
// Start a transaction for the whole save.
|
||||||
|
$transaction = $entity->getDatabase()->startTransaction();
|
||||||
|
|
||||||
|
// Get all auto values for undefined tables or keys.
|
||||||
|
[$table, $foreignKey, $foreignValue] = $this->getState($entity->getTableName());
|
||||||
|
|
||||||
|
// Get entity primary key value.
|
||||||
|
$entityKey = $entity->{array_first_or_val($entity->getPrimaryFields())};
|
||||||
|
|
||||||
|
// Delete existing values.
|
||||||
|
((new DeleteQuery($entity->getDatabase(), $table))
|
||||||
|
->where($foreignKey, "=", $entityKey))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
// Get actual columns names.
|
||||||
|
$foreignKey = $this->getColumnName($foreignKey);
|
||||||
|
$foreignValue = $this->getColumnName($foreignValue);
|
||||||
|
|
||||||
|
// Insert new ones.
|
||||||
|
((new InsertQuery($entity->getDatabase(), $table)))
|
||||||
|
// Insert all values of the array.
|
||||||
|
->values(...array_map(fn (mixed $val) => ([
|
||||||
|
$foreignKey => $entityKey,
|
||||||
|
$foreignValue => $val,
|
||||||
|
]), $value))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
// Commit transaction.
|
||||||
|
$transaction->commit();
|
||||||
|
}
|
||||||
|
}
|
125
src/Entities.php
Normal file
125
src/Entities.php
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Database\Exceptions\Query\MissingConditionValueException;
|
||||||
|
use Nest\Database\Exceptions\Query\NoPrimaryFieldException;
|
||||||
|
use Nest\Database\Exceptions\UnknownDatabaseException;
|
||||||
|
use Nest\Exceptions\InvalidTypeException;
|
||||||
|
use Nest\Model\Exceptions\MissingRequiredFieldException;
|
||||||
|
use Nest\Model\Exceptions\UndefinedRelationException;
|
||||||
|
use Nest\Types\Exceptions\IncompatibleTypeException;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
use function Nest\Utils\array_first;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class of a collection of entities, to perform operations on all of them.
|
||||||
|
* @template Entity
|
||||||
|
*/
|
||||||
|
class Entities
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Entities.
|
||||||
|
* @var Entity[]
|
||||||
|
*/
|
||||||
|
protected array $entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a collection of entities with an array of entities.
|
||||||
|
* @param Entity[] $entities
|
||||||
|
*/
|
||||||
|
public function __construct(array $entities)
|
||||||
|
{
|
||||||
|
$this->entities = $entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entities array.
|
||||||
|
* @return Entity[] The entities.
|
||||||
|
*/
|
||||||
|
public function get(): array
|
||||||
|
{
|
||||||
|
return $this->entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set values to all the entities.
|
||||||
|
* @param string $name Name of the value to set.
|
||||||
|
* @param mixed $value Value to set.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __set(string $name, mixed $value): void
|
||||||
|
{
|
||||||
|
foreach ($this->entities as $entity)
|
||||||
|
$entity->$name = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all values with the given name in the entities.
|
||||||
|
* @param string $name Name of the value to get.
|
||||||
|
* @return array Array of values.
|
||||||
|
*/
|
||||||
|
public function __get(string $name): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($this->entities as $entity)
|
||||||
|
$result[] = $entity->$name;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the given method on all entities.
|
||||||
|
* @param string $name Name of the method to call.
|
||||||
|
* @param array $arguments Arguments of the method.
|
||||||
|
* @return array Results of all calls.
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
foreach ($this->entities as $entity)
|
||||||
|
$results[] = $entity->$name(...$arguments);
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current entities states.
|
||||||
|
* @return bool True if something was saved, false otherwise.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws MissingRequiredFieldException
|
||||||
|
* @throws NoPrimaryFieldException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
*/
|
||||||
|
public function save(): bool
|
||||||
|
{
|
||||||
|
$result = false;
|
||||||
|
foreach ($this->entities as $entity)
|
||||||
|
// Set result to true if needed.
|
||||||
|
$result = $result || $entity->save();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load given relation in the entities.
|
||||||
|
* @param array<string|int, string|callable(EntityQuery): void> $relations Relations to load.
|
||||||
|
* @return $this
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
*/
|
||||||
|
public function load(array $relations): static
|
||||||
|
{
|
||||||
|
// Do nothing if there are no entities.
|
||||||
|
if (empty($this->entities)) return $this;
|
||||||
|
|
||||||
|
// Normalize relations array.
|
||||||
|
$relations = EntityQuery::normalizeRelationsDefinition($relations);
|
||||||
|
|
||||||
|
// Perform load for all the entities.
|
||||||
|
array_first($this->entities)->query()->load($this->entities, $relations);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
586
src/Entity.php
Normal file
586
src/Entity.php
Normal file
|
@ -0,0 +1,586 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Nest\Application;
|
||||||
|
use Nest\Database\Database;
|
||||||
|
use Nest\Database\Query\QueryBuilder;
|
||||||
|
use Nest\Database\Query\Raw;
|
||||||
|
use Nest\Events\HasEvents;
|
||||||
|
use Nest\Database\Exceptions\Query\MissingConditionValueException;
|
||||||
|
use Nest\Database\Exceptions\Query\NoPrimaryFieldException;
|
||||||
|
use Nest\Database\Exceptions\UnknownDatabaseException;
|
||||||
|
use Nest\Exceptions\InvalidTypeException;
|
||||||
|
use Nest\Model\Exceptions\MissingRequiredFieldException;
|
||||||
|
use Nest\Model\Exceptions\UndefinedRelationException;
|
||||||
|
use Nest\Model\Exceptions\UnhandledPropertyTypeException;
|
||||||
|
use Nest\Types\Exceptions\IncompatibleTypeException;
|
||||||
|
use Nest\Model\Events\AfterDelete;
|
||||||
|
use Nest\Model\Events\AfterInsert;
|
||||||
|
use Nest\Model\Events\AfterLoad;
|
||||||
|
use Nest\Model\Events\AfterPropertiesInitialization;
|
||||||
|
use Nest\Model\Events\AfterSave;
|
||||||
|
use Nest\Model\Events\AfterUpdate;
|
||||||
|
use Nest\Model\Events\BeforeDelete;
|
||||||
|
use Nest\Model\Events\BeforeInsert;
|
||||||
|
use Nest\Model\Events\BeforeLoad;
|
||||||
|
use Nest\Model\Events\BeforePropertiesInitialization;
|
||||||
|
use Nest\Model\Events\BeforeSave;
|
||||||
|
use Nest\Model\Events\BeforeUpdate;
|
||||||
|
use Nest\Model\Events\EntityDefinitionEvent;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
use Nest\Types\SqlType;
|
||||||
|
use function Nest\Utils\array_first;
|
||||||
|
use function Nest\Utils\get_class_name_from_fullname;
|
||||||
|
use function Nest\Utils\str_camel_to_snake;
|
||||||
|
use function Nest\Utils\str_snake_pluralize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class of an entity of the model.
|
||||||
|
*/
|
||||||
|
abstract class Entity
|
||||||
|
{
|
||||||
|
use HasEvents;
|
||||||
|
/**
|
||||||
|
* Set if the entity is new (should be inserted) or not.
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected bool $isNew = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original properties values.
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $originals = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity definition cache.
|
||||||
|
* @var EntityBlueprint
|
||||||
|
*/
|
||||||
|
private EntityBlueprint $_definition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of the entity.
|
||||||
|
* @param EntityBlueprint $blueprint Entity blueprint to define.
|
||||||
|
* @return EntityBlueprint Defined entity blueprint.
|
||||||
|
*/
|
||||||
|
public abstract function definition(EntityBlueprint $blueprint): EntityBlueprint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entity definition (from cache if already requested).
|
||||||
|
* @return EntityBlueprint Defined entity blueprint.
|
||||||
|
*/
|
||||||
|
private function _getDefinition(): EntityBlueprint
|
||||||
|
{
|
||||||
|
if (empty($this->_definition))
|
||||||
|
{ // Get a new definition and save it in cached definition.
|
||||||
|
$this->_definition = $this->definition(new EntityBlueprint(static::class));
|
||||||
|
$this->getEventsManager()->fire(new EntityDefinitionEvent($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return cached definition.
|
||||||
|
return $this->_definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize entity.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Initialize properties.
|
||||||
|
$this->initializeProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize entity default properties.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function initializeProperties(): void
|
||||||
|
{
|
||||||
|
$this->getEventsManager()->fire(new BeforePropertiesInitialization($this));
|
||||||
|
|
||||||
|
foreach ($this->_getDefinition()->getProperties() as $propertyName => $property)
|
||||||
|
{ // For each property, set its default value if there is one.
|
||||||
|
if ($property instanceof ReadableFieldBlueprint && $property->hasCurrentDateByDefault())
|
||||||
|
$this->$propertyName = Carbon::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterPropertiesInitialization($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guess the entity database table name from its class name.
|
||||||
|
* Always use a plural snake_cased name.
|
||||||
|
* @return string The database table name of the entity, based on its class name.
|
||||||
|
*/
|
||||||
|
private function guessTableName(): string
|
||||||
|
{
|
||||||
|
return str_snake_pluralize(str_camel_to_snake(get_class_name_from_fullname(
|
||||||
|
(new \ReflectionClass($this))->isAnonymous() ? get_parent_class(static::class) : static::class,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entity database table name.
|
||||||
|
* @return string The database table name of the entity.
|
||||||
|
*/
|
||||||
|
public function getTableName(): string
|
||||||
|
{
|
||||||
|
return $this->_getDefinition()->getTable() ?? $this->guessTableName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the required fields are set.
|
||||||
|
* @return string[] Names of the missing required fields.
|
||||||
|
*/
|
||||||
|
public function getMissingRequiredFields(): array
|
||||||
|
{
|
||||||
|
// Get current entity definition.
|
||||||
|
$entityBlueprint = $this->_getDefinition();
|
||||||
|
// Get its required fields.
|
||||||
|
$requiredFields = array_filter($entityBlueprint->getProperties(), fn ($field) => $field instanceof ReadableFieldBlueprint && $field->isRequired());
|
||||||
|
|
||||||
|
// Initialize missing required fields list.
|
||||||
|
$missingRequiredFields = [];
|
||||||
|
|
||||||
|
foreach ($requiredFields as $requiredFieldName => $requiredField)
|
||||||
|
{ // If any required field is not set, add it in the list.
|
||||||
|
if (!isset($this->{$requiredFieldName}))
|
||||||
|
// A required field is not set.
|
||||||
|
$missingRequiredFields[] = $requiredFieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $missingRequiredFields; // All the missing required fields.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that all the required fields are set.
|
||||||
|
* Throw an exception if some are not set.
|
||||||
|
* @return void
|
||||||
|
* @throws MissingRequiredFieldException
|
||||||
|
*/
|
||||||
|
public function checkRequiredFields(): void
|
||||||
|
{
|
||||||
|
if (!empty($missingFields = $this->getMissingRequiredFields()))
|
||||||
|
// Some required fields are missing, throwing an exception.
|
||||||
|
throw new MissingRequiredFieldException($missingFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get primary fields.
|
||||||
|
* @return string|string[] The primary field name, or array of fields when there are multiple primary fields.
|
||||||
|
*/
|
||||||
|
public function getPrimaryFields(): string|array
|
||||||
|
{
|
||||||
|
// Search primary fields.
|
||||||
|
$primaryFields = array_filter($this->_getDefinition()->getProperties(), fn ($field) => $field instanceof ReadableFieldBlueprint && $field->isPrimary());
|
||||||
|
|
||||||
|
// Return primary field(s).
|
||||||
|
return count($primaryFields) == 1 ? array_key_first($primaryFields) : array_keys($primaryFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database of the entity.
|
||||||
|
* @return Database Database of the entity.
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function getDatabase(): Database
|
||||||
|
{
|
||||||
|
return Application::get()->databases()->db($this->_getDefinition()->getDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SQL query for the entity.
|
||||||
|
* @return QueryBuilder SQL query builder.
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function sql(): QueryBuilder
|
||||||
|
{
|
||||||
|
return $this->getDatabase()->query($this->getTableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new entity query.
|
||||||
|
* @return EntityQuery<static> Entity query builder.
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function query(): EntityQuery
|
||||||
|
{
|
||||||
|
// Create the query.
|
||||||
|
$query = new EntityQuery($this->getDatabase(), $this->getTableName(), $this);
|
||||||
|
|
||||||
|
// Automatically fill "with" with eager loaded properties.
|
||||||
|
$query->with(array_filter(array_map(fn ($property) => (
|
||||||
|
$property instanceof ReadableEntityPropertyBlueprint && $property->doEagerLoad()
|
||||||
|
), $this->_getDefinition()->getProperties())));
|
||||||
|
|
||||||
|
return $query; // Return the created query.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load given relations in the entity.
|
||||||
|
* @param array<string|int, string|callable(EntityQuery): void> $relations Relations to load.
|
||||||
|
* @return $this
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
*/
|
||||||
|
public function load(array $relations): static
|
||||||
|
{
|
||||||
|
// Normalize relations array.
|
||||||
|
$relations = EntityQuery::normalizeRelationsDefinition($relations);
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new BeforeLoad($this, $relations));
|
||||||
|
|
||||||
|
// Perform load for the current entity.
|
||||||
|
$this->query()->load([$this], $relations);
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterLoad($this, $relations));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new entity of the same type.
|
||||||
|
* @return static A new entity of the same type.
|
||||||
|
*/
|
||||||
|
public function new(): static
|
||||||
|
{
|
||||||
|
return new static();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an original property value.
|
||||||
|
* @param string $propertyName Property name.
|
||||||
|
* @param mixed $propertyValue Property value.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setOriginalProperty(string $propertyName, mixed $propertyValue): void
|
||||||
|
{
|
||||||
|
$this->$propertyName = $propertyValue;
|
||||||
|
$this->originals[$propertyName] = $propertyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find changes made on the entity.
|
||||||
|
* @return array<string, mixed> New properties values.
|
||||||
|
*/
|
||||||
|
public function diff(): array
|
||||||
|
{
|
||||||
|
// Initialize diff array.
|
||||||
|
$diff = [];
|
||||||
|
|
||||||
|
foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition)
|
||||||
|
{ // Add current property value to diff if it's different from original value.
|
||||||
|
if (!($propertyDefinition instanceof FieldBlueprint || $propertyDefinition instanceof ArrayBlueprint))
|
||||||
|
// Only keep normal fields.
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (isset($this->$propertyName) && (!isset($this->originals[$propertyName]) || $this->originals[$propertyName] != $this->$propertyName))
|
||||||
|
// Current value is different from original one, adding it to the diff.
|
||||||
|
$diff[$propertyName] = $this->$propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff; // Return properties diff.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current entity state.
|
||||||
|
* @return bool True if something was saved, false otherwise.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws MissingRequiredFieldException
|
||||||
|
* @throws NoPrimaryFieldException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function save(): bool
|
||||||
|
{
|
||||||
|
$this->getEventsManager()->fire(new BeforeSave($this));
|
||||||
|
|
||||||
|
// Check required fields to save.
|
||||||
|
$this->checkRequiredFields();
|
||||||
|
|
||||||
|
if ($this->isNew)
|
||||||
|
{ // This is a new entity, inserting it, then reading the result.
|
||||||
|
$this->fromSqlProperties($this->insert());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // The entity already exists, updating it.
|
||||||
|
|
||||||
|
// Get entity diff.
|
||||||
|
$diff = $this->diff();
|
||||||
|
|
||||||
|
// No diff, return false because there is nothing to update.
|
||||||
|
if (empty($diff)) return false;
|
||||||
|
|
||||||
|
// Get properties definition.
|
||||||
|
$properties = $this->_getDefinition()->getProperties();
|
||||||
|
|
||||||
|
foreach ($diff as $propertyName => $propertyValue)
|
||||||
|
{ // For each property in diff, check if it's an array field.
|
||||||
|
if ($properties[$propertyName] instanceof ArrayBlueprint)
|
||||||
|
{ // If the property is an array field, save it then remove it from diff.
|
||||||
|
(new ArrayInstance($properties[$propertyName]))->save($this, $propertyValue);
|
||||||
|
unset($diff[$propertyName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update every else property (if there are still some).
|
||||||
|
if (!empty($diff))
|
||||||
|
$this->fromSqlProperties($this->update($diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterSave($this));
|
||||||
|
|
||||||
|
return true; // Entity saved.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the entity.
|
||||||
|
* @return bool
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws NoPrimaryFieldException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function delete(): bool
|
||||||
|
{
|
||||||
|
$this->getEventsManager()->fire(new BeforeDelete($this));
|
||||||
|
|
||||||
|
// Delete current entity.
|
||||||
|
$this->sql()->newDelete()->whereKeyOf($this)->execute();
|
||||||
|
|
||||||
|
// Set entity as new, in case of a save after deletion.
|
||||||
|
$this->isNew = true;
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterDelete($this));
|
||||||
|
|
||||||
|
return true; // Entity is deleted.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert current entity values.
|
||||||
|
* @return object Raw inserted object.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
private function insert(): object
|
||||||
|
{
|
||||||
|
$this->getEventsManager()->fire(new BeforeInsert($this));
|
||||||
|
|
||||||
|
// Return the first result (= the sole inserted row).
|
||||||
|
$result = array_first(
|
||||||
|
// Execute INSERT in RETURNING mode.
|
||||||
|
$this->sql()->newInsert()->values(
|
||||||
|
// Extract properties in an SQL row values array.
|
||||||
|
$this->toSqlProperties()
|
||||||
|
)->execute(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterInsert($this));
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update given entity values.
|
||||||
|
* @param array<string, mixed> $values Values to update.
|
||||||
|
* @return object Raw updated object.
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws NoPrimaryFieldException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
private function update(array $values): object
|
||||||
|
{
|
||||||
|
$this->getEventsManager()->fire(new BeforeUpdate($this));
|
||||||
|
|
||||||
|
// Return the first result (= the sole updated row).
|
||||||
|
$result = array_first(
|
||||||
|
// Execute UPDATE in RETURNING mode.
|
||||||
|
$this->sql()->newUpdate()->set($this->valuesToSql($values))
|
||||||
|
->whereKeyOf($this)
|
||||||
|
->execute(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->getEventsManager()->fire(new AfterUpdate($this));
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get relation of the given name.
|
||||||
|
* @param string $relationName Name of the relation to get.
|
||||||
|
* @return EntityPropertyInstance The property instance.
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
*/
|
||||||
|
public function getRelation(string $relationName): EntityPropertyInstance
|
||||||
|
{
|
||||||
|
// Get defined properties for the entity.
|
||||||
|
$properties = $this->_getDefinition()->getProperties();
|
||||||
|
|
||||||
|
if (!empty($properties[$relationName]))
|
||||||
|
{ // A property with the relation name exists, trying to get its relation definition.
|
||||||
|
$definition = $properties[$relationName];
|
||||||
|
if (is_a($definition, EntityPropertyBlueprint::class))
|
||||||
|
// Initialize a property instance.
|
||||||
|
return new EntityPropertyInstance($definition);
|
||||||
|
else
|
||||||
|
{ // Not an entity blueprint, throw an exception.
|
||||||
|
throw new UndefinedRelationException(static::class, $relationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // No property with this name, throw an exception.
|
||||||
|
throw new UndefinedRelationException(static::class, $relationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert given values to SQL from matching properties types.
|
||||||
|
* @param array<string, mixed> $values Values to convert to SQL value.
|
||||||
|
* @return array<string, Raw|string|int|float|null> SQL values.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
*/
|
||||||
|
private function valuesToSql(array $values): array
|
||||||
|
{
|
||||||
|
// Get properties definition.
|
||||||
|
$propertiesDefinition = $this->_getDefinition()->getProperties();
|
||||||
|
|
||||||
|
foreach ($values as $propertyName => $value)
|
||||||
|
{ // For each value, try to convert its data.
|
||||||
|
|
||||||
|
// Keep the value (and convert it) if there is a matching property.
|
||||||
|
if (!empty($propertiesDefinition[$propertyName]))
|
||||||
|
{
|
||||||
|
// Get property type.
|
||||||
|
$propertyType = $propertiesDefinition[$propertyName]->getTypeInstance();
|
||||||
|
|
||||||
|
if (!$propertyType instanceof SqlType)
|
||||||
|
// Type is not an SQL type, throwing an exception.
|
||||||
|
throw new IncompatibleTypeException(get_class($propertyType), SqlType::class);
|
||||||
|
|
||||||
|
// Converting property value and put it in the SQL object.
|
||||||
|
$values[$propertyName] = $propertyType->toSql($value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// No property matching the current value, unset it.
|
||||||
|
unset($values[$propertyName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $values; // Return converted values.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import properties from a raw SQL object.
|
||||||
|
* @param object $sqlProperties Raw SQL object where to extract properties.
|
||||||
|
* @param string $prefix Optional prefix of properties in SQL.
|
||||||
|
* @return void
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
*/
|
||||||
|
public function fromSqlProperties(object $sqlProperties, string $prefix = ""): void
|
||||||
|
{
|
||||||
|
$this->isNew = false; // The entity already exists, as it's read from SQL properties.
|
||||||
|
|
||||||
|
foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition)
|
||||||
|
{ // For each defined property, try to read its data.
|
||||||
|
// Prepend property name by the prefix.
|
||||||
|
$propertyName = $prefix.$propertyName;
|
||||||
|
|
||||||
|
// Try to read only if a value is present.
|
||||||
|
if (isset($sqlProperties->$propertyName))
|
||||||
|
{ // Get property type.
|
||||||
|
if (!($propertyDefinition instanceof FieldBlueprint || $propertyDefinition instanceof ArrayBlueprint))
|
||||||
|
throw new UnhandledPropertyTypeException($propertyName, get_class($propertyDefinition));
|
||||||
|
|
||||||
|
// Get property type instance.
|
||||||
|
$propertyType = $propertyDefinition->getTypeInstance();
|
||||||
|
|
||||||
|
if (!$propertyType instanceof SqlType)
|
||||||
|
// Type is not an SQL type, throwing an exception.
|
||||||
|
throw new IncompatibleTypeException(get_class($propertyType), SqlType::class);
|
||||||
|
|
||||||
|
if ($propertyDefinition instanceof FieldBlueprint)
|
||||||
|
{ // Read direct value.
|
||||||
|
// Parsing property value and put it in the entity.
|
||||||
|
$value = $propertyType->fromSql($sqlProperties->$propertyName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ // Read JSON array value.
|
||||||
|
$value = json_decode($sqlProperties->$propertyName);
|
||||||
|
// Parsing array values.
|
||||||
|
foreach ($value as &$val) $val = $propertyType->fromSql($val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set original property value.
|
||||||
|
$this->setOriginalProperty($propertyName, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export properties to a raw SQL object.
|
||||||
|
* @return array<string, Raw|string|int|float|null> Raw SQL array where properties are extracted.
|
||||||
|
* @param string $prefix Optional prefix of properties in SQL.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
*/
|
||||||
|
public function toSqlProperties(string $prefix = ""): array
|
||||||
|
{
|
||||||
|
// Initialize raw SQL array.
|
||||||
|
$object = [];
|
||||||
|
|
||||||
|
foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition)
|
||||||
|
{ // Get SQL value of each defined property.
|
||||||
|
// Prepend property name by the prefix.
|
||||||
|
$propertyName = $prefix.$propertyName;
|
||||||
|
|
||||||
|
if (isset($this->$propertyName))
|
||||||
|
{ // If property is set, converting it.
|
||||||
|
// Get property type.
|
||||||
|
$propertyType = $propertyDefinition->getTypeInstance();
|
||||||
|
|
||||||
|
if (!$propertyType instanceof SqlType)
|
||||||
|
// Type is not an SQL type, throwing an exception.
|
||||||
|
throw new IncompatibleTypeException(get_class($propertyType), SqlType::class);
|
||||||
|
|
||||||
|
// Converting property value and put it in the SQL object.
|
||||||
|
$object[$propertyName] = $propertyType->toSql($this->$propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object; // Return built raw SQL array.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate columns SELECT for the defined properties.
|
||||||
|
* @param string $prefix Prefix to add to the name of the properties.
|
||||||
|
* @return Raw[] Selected columns.
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
*/
|
||||||
|
public function sqlSelectFields(string $prefix = ""): array
|
||||||
|
{
|
||||||
|
$select = [];
|
||||||
|
|
||||||
|
foreach ($this->_getDefinition()->getProperties() as $propertyName => $property)
|
||||||
|
{ // For each field, generate a column selection.
|
||||||
|
if ($property instanceof FieldBlueprint)
|
||||||
|
{ // It's a field property, generate a SELECT for it.
|
||||||
|
$select[] = new Raw("\"{$this->getTableName()}\".\"$propertyName\"".(!empty($prefix) ? " AS \"$prefix$propertyName\"" : ""));
|
||||||
|
}
|
||||||
|
elseif ($property instanceof ArrayBlueprint)
|
||||||
|
{ // It's an array field, generate a SELECT for it.
|
||||||
|
$select[] = (new ArrayInstance($property))->genSelect($this->query(), $prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $select;
|
||||||
|
}
|
||||||
|
}
|
155
src/EntityBlueprint.php
Normal file
155
src/EntityBlueprint.php
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Types\Definitions\CarbonType;
|
||||||
|
use Nest\Types\Definitions\Integers\BigintType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
class EntityBlueprint
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Entity class.
|
||||||
|
* @var class-string<T>
|
||||||
|
*/
|
||||||
|
protected string $entityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database identifier of the entity.
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected ?string $database = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database table name of the entity.
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected ?string $table = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties definition array.
|
||||||
|
* @var array<string, (ReadableFieldBlueprint|ReadableArrayBlueprint|ReadableEntityPropertyBlueprint)>
|
||||||
|
*/
|
||||||
|
protected array $properties = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string<T> $entityClass Entity class.
|
||||||
|
*/
|
||||||
|
public function __construct(string $entityClass)
|
||||||
|
{
|
||||||
|
$this->entityClass = $entityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the database table name of the entity.
|
||||||
|
* @param string $table The database table name of the entity.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setTable(string $table): static
|
||||||
|
{
|
||||||
|
$this->table = $table;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the database table name of the entity.
|
||||||
|
* @return string|null The database table name.
|
||||||
|
*/
|
||||||
|
public function getTable(): ?string
|
||||||
|
{
|
||||||
|
return $this->table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the database of the entity.
|
||||||
|
* @param string $databaseIdentifier The database identifier.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setDatabase(string $databaseIdentifier): static
|
||||||
|
{
|
||||||
|
$this->database = $databaseIdentifier;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the database of the entity.
|
||||||
|
* @return string|null The database identifier.
|
||||||
|
*/
|
||||||
|
public function getDatabase(): ?string
|
||||||
|
{
|
||||||
|
return $this->database ?? "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the defined properties.
|
||||||
|
* @return array<string, (ReadableFieldBlueprint|ReadableArrayBlueprint|ReadableEntityPropertyBlueprint)> The defined properties.
|
||||||
|
*/
|
||||||
|
public function getProperties(): array
|
||||||
|
{
|
||||||
|
return $this->properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function field(string $name, string $type): FieldBlueprint
|
||||||
|
{
|
||||||
|
return $this->properties[$name] = (new ReadableFieldBlueprint())->type($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function id(string $name = "id", string $type = BigintType::class): FieldBlueprint
|
||||||
|
{
|
||||||
|
return $this->foreignId($name, $type)->autoIncrement()->primary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function foreignId(string $name, string $type = BigintType::class): FieldBlueprint
|
||||||
|
{
|
||||||
|
return $this->field($name, $type)->unsigned()->index();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createdAt(string $name = "created_at", string $type = CarbonType::class): FieldBlueprint
|
||||||
|
{
|
||||||
|
return $this->field($name, $type)->required()->currentDateByDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedAt(string $name = "updated_at", string $type = CarbonType::class): FieldBlueprint
|
||||||
|
{
|
||||||
|
return $this->field($name, $type)->required()->currentDateByDefault()->currentDateOnUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an array field.
|
||||||
|
* @param string $name The name of the field.
|
||||||
|
* @param string $type The type of the array values.
|
||||||
|
* @return ArrayBlueprint The array blueprint.
|
||||||
|
*/
|
||||||
|
public function array(string $name, string $type): ArrayBlueprint
|
||||||
|
{
|
||||||
|
return $this->properties[$name] = (new ReadableArrayBlueprint($this->entityClass, $name))->type($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an entity property.
|
||||||
|
* @param string $name The name of the property.
|
||||||
|
* @param string $class The class of the property.
|
||||||
|
* @param bool $multiple Whether the property is an array or not.
|
||||||
|
* @return EntityPropertyBlueprint The property blueprint.
|
||||||
|
*/
|
||||||
|
public function entity(string $name, string $class, bool $multiple = false): EntityPropertyBlueprint
|
||||||
|
{
|
||||||
|
return $this->properties[$name] = (new ReadableEntityPropertyBlueprint())->class($class)->multiple($multiple);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an entities array property.
|
||||||
|
* @param string $name The name of the property.
|
||||||
|
* @param string $class The class of the property.
|
||||||
|
* @return EntityPropertyBlueprint The property blueprint.
|
||||||
|
*/
|
||||||
|
public function entities(string $name, string $class): EntityPropertyBlueprint
|
||||||
|
{
|
||||||
|
return $this->entity($name, $class, true);
|
||||||
|
}
|
||||||
|
}
|
106
src/EntityPropertyBlueprint.php
Normal file
106
src/EntityPropertyBlueprint.php
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
abstract class EntityPropertyBlueprint
|
||||||
|
{
|
||||||
|
protected string $class;
|
||||||
|
|
||||||
|
protected bool $multiple;
|
||||||
|
|
||||||
|
protected bool $allowInline = true;
|
||||||
|
|
||||||
|
protected EntityPropertyMode $mode;
|
||||||
|
|
||||||
|
protected ?string $localKey = null;
|
||||||
|
|
||||||
|
protected ?string $relatedKey = null;
|
||||||
|
|
||||||
|
protected ?string $pivotTable = null;
|
||||||
|
|
||||||
|
protected ?string $pivotLocalKey = null;
|
||||||
|
|
||||||
|
protected ?string $pivotRelatedKey = null;
|
||||||
|
|
||||||
|
protected bool $eagerLoad = false;
|
||||||
|
|
||||||
|
public function class(string $class): static
|
||||||
|
{
|
||||||
|
$this->class = $class;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function multiple(bool $multiple = true): static
|
||||||
|
{
|
||||||
|
$this->multiple = $multiple;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if inline loading of the relation is allowed or not.
|
||||||
|
* @param bool $inline True to allow relation inline loading.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function allowInline(bool $inline = true): static
|
||||||
|
{
|
||||||
|
$this->allowInline = $inline;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromLocal(?string $localKey = null, ?string $relatedKey = null): static
|
||||||
|
{
|
||||||
|
$this->mode = EntityPropertyMode::LOCAL;
|
||||||
|
$this->localKey = $localKey;
|
||||||
|
$this->relatedKey = $relatedKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function fromRelated(?string $relatedKey = null, ?string $localKey = null): static
|
||||||
|
{
|
||||||
|
$this->mode = EntityPropertyMode::RELATED;
|
||||||
|
$this->localKey = $localKey;
|
||||||
|
$this->relatedKey = $relatedKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function localKey(string $localKey): static
|
||||||
|
{
|
||||||
|
$this->localKey = $localKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function relatedKey(string $relatedKey): static
|
||||||
|
{
|
||||||
|
$this->relatedKey = $relatedKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromPivot(string $pivotTable, ?string $pivotLocalKey = null, ?string $pivotRelatedKey = null): static
|
||||||
|
{
|
||||||
|
$this->mode = EntityPropertyMode::PIVOT;
|
||||||
|
$this->pivotLocalKey = $pivotLocalKey;
|
||||||
|
$this->pivotRelatedKey = $pivotRelatedKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pivotLocalKey(string $pivotLocalKey): static
|
||||||
|
{
|
||||||
|
$this->pivotLocalKey = $pivotLocalKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function pivotRelatedKey(string $pivotRelatedKey): static
|
||||||
|
{
|
||||||
|
$this->pivotRelatedKey = $pivotRelatedKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if the related field should be eager loaded or not.
|
||||||
|
* Only apply on related fields.
|
||||||
|
* @param bool $load True to eager load the field.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function eagerLoad(bool $load = true): static
|
||||||
|
{
|
||||||
|
$this->eagerLoad = $load;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
200
src/EntityPropertyInstance.php
Normal file
200
src/EntityPropertyInstance.php
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Database\Query\Raw;
|
||||||
|
use Nest\Database\Exceptions\UnknownDatabaseException;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
use function Nest\Utils\array_first;
|
||||||
|
use function Nest\Utils\array_first_or_val;
|
||||||
|
use function Nest\Utils\str_snake_singularize;
|
||||||
|
|
||||||
|
class EntityPropertyInstance extends ReadableEntityPropertyBlueprint
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Goal entity instance.
|
||||||
|
* @var Entity
|
||||||
|
*/
|
||||||
|
private Entity $entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an entity property instance from an entity property definition.
|
||||||
|
* @param EntityPropertyBlueprint $entityProperty The entity property to copy from.
|
||||||
|
*/
|
||||||
|
public function __construct(EntityPropertyBlueprint $entityProperty)
|
||||||
|
{
|
||||||
|
// Copy all fields.
|
||||||
|
$this->class = $entityProperty->class;
|
||||||
|
$this->multiple = $entityProperty->multiple;
|
||||||
|
$this->allowInline = $entityProperty->allowInline;
|
||||||
|
$this->mode = $entityProperty->mode;
|
||||||
|
$this->localKey = $entityProperty->localKey;
|
||||||
|
$this->relatedKey = $entityProperty->relatedKey;
|
||||||
|
$this->pivotTable = $entityProperty->pivotTable;
|
||||||
|
$this->pivotLocalKey = $entityProperty->pivotLocalKey;
|
||||||
|
$this->pivotRelatedKey = $entityProperty->pivotRelatedKey;
|
||||||
|
$this->eagerLoad = $entityProperty->eagerLoad;
|
||||||
|
|
||||||
|
// Initialize goal entity.
|
||||||
|
$this->entity = new ($this->getClass())();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new related entity.
|
||||||
|
* @return Entity A new related entity instance.
|
||||||
|
*/
|
||||||
|
public function newEntity(): Entity
|
||||||
|
{
|
||||||
|
return $this->entity->new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the property can be inline loaded.
|
||||||
|
* @return bool True if the property can be inline loaded.
|
||||||
|
*/
|
||||||
|
public function canInlineLoad(): bool
|
||||||
|
{
|
||||||
|
return !$this->isMultiple() && $this->isInlineAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a query to retrieve relations for the given entities.
|
||||||
|
* @param Entity[] $entities Entities for which to retrieve relations.
|
||||||
|
* @return EntityQuery Built entities query.
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function queryFor(array $entities): EntityQuery
|
||||||
|
{
|
||||||
|
// Initialize query.
|
||||||
|
$query = $this->entity->query();
|
||||||
|
|
||||||
|
if (empty($entities))
|
||||||
|
// No entities, return a simple query without conditions.
|
||||||
|
return $query;
|
||||||
|
|
||||||
|
// Get a reference entity.
|
||||||
|
$referenceEntity = array_first($entities);
|
||||||
|
// Entity primary key.
|
||||||
|
$entityPrimaryKey = array_first_or_val($referenceEntity->getPrimaryFields());
|
||||||
|
|
||||||
|
// Get keys from entities list.
|
||||||
|
$entitiesKeys = array_column($entities, $entityPrimaryKey);
|
||||||
|
|
||||||
|
switch ($this->getMode())
|
||||||
|
{ // Build query depending on the relation mode.
|
||||||
|
case EntityPropertyMode::LOCAL:
|
||||||
|
$query
|
||||||
|
->select(new Raw("\"{$referenceEntity->getTableName()}\".\"$entityPrimaryKey\" AS \"__reference_key\""))
|
||||||
|
// Generate SELECT for the related entity.
|
||||||
|
->select(...$this->entity->sqlSelectFields())
|
||||||
|
->innerJoin($referenceEntity->getTableName())->on(
|
||||||
|
"{$this->entity->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields())
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$referenceEntity->getTableName()}.".(
|
||||||
|
$this->getLocalKey() ?? str_snake_singularize($this->entity->getTableName())."_id"
|
||||||
|
),
|
||||||
|
)->whereIn(
|
||||||
|
"{$referenceEntity->getTableName()}.$entityPrimaryKey", $entitiesKeys
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityPropertyMode::RELATED:
|
||||||
|
$query
|
||||||
|
->select(new Raw("\"{$this->entity->getTableName()}\".\"".(
|
||||||
|
$this->getRelatedKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id"
|
||||||
|
)."\" AS \"__reference_key\""))
|
||||||
|
// Generate SELECT for the related entity.
|
||||||
|
->select(...$this->entity->sqlSelectFields())
|
||||||
|
->whereIn(
|
||||||
|
"{$this->entity->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id"
|
||||||
|
)."",
|
||||||
|
$entitiesKeys
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityPropertyMode::PIVOT:
|
||||||
|
$query
|
||||||
|
->select(new Raw("\"{$this->entity->getTableName()}\".\"".(
|
||||||
|
$this->getPivotLocalKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id"
|
||||||
|
)."\" AS \"__reference_key\""))
|
||||||
|
// Generate SELECT for the related entity.
|
||||||
|
->select(...$this->entity->sqlSelectFields())
|
||||||
|
->innerJoin($this->getPivotTable())->on(
|
||||||
|
"{$query->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? $query->getPrimaryKeyName()
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$this->getPivotTable()}.".(
|
||||||
|
$this->getPivotLocalKey() ?? str_snake_singularize($query->getTableName())."_id"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
->whereIn(
|
||||||
|
"{$this->getPivotTable()}.".(
|
||||||
|
$this->getPivotLocalKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id"
|
||||||
|
),
|
||||||
|
$entitiesKeys
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup inline loading for the current property in an entity query.
|
||||||
|
* @param EntityQuery $query The entity query to alter.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setupInlineLoading(EntityQuery $query): void
|
||||||
|
{
|
||||||
|
switch ($this->getMode())
|
||||||
|
{ // Add join depending on the relation mode.
|
||||||
|
case EntityPropertyMode::LOCAL:
|
||||||
|
$query
|
||||||
|
->leftJoin($this->entity->getTableName())->on(
|
||||||
|
"{$query->getTableName()}.".(
|
||||||
|
$this->getLocalKey() ?? str_snake_singularize($this->entity->getTableName())."_id"
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$this->entity->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityPropertyMode::RELATED:
|
||||||
|
$query
|
||||||
|
->leftJoin($this->entity->getTableName())->on(
|
||||||
|
"{$query->getTableName()}.".(
|
||||||
|
$this->getLocalKey() ?? $query->getPrimaryKeyName()
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$this->entity->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? str_snake_singularize($query->getTableName())."_id"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case EntityPropertyMode::PIVOT:
|
||||||
|
$query
|
||||||
|
->leftJoin($this->getPivotTable())->on(
|
||||||
|
"{$query->getTableName()}.".(
|
||||||
|
$this->getLocalKey() ?? $query->getPrimaryKeyName()
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$this->getPivotTable()}.".(
|
||||||
|
$this->getPivotLocalKey() ?? str_snake_singularize($query->getTableName())."_id"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
->leftJoin($this->entity->getTableName())->on(
|
||||||
|
"{$this->getPivotTable()}.".(
|
||||||
|
$this->getPivotRelatedKey() ?? str_snake_singularize($this->entity->getTableName())."_id"
|
||||||
|
),
|
||||||
|
"=",
|
||||||
|
"{$this->entity->getTableName()}.".(
|
||||||
|
$this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/EntityPropertyMode.php
Normal file
10
src/EntityPropertyMode.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
enum EntityPropertyMode: string
|
||||||
|
{
|
||||||
|
case LOCAL = "local";
|
||||||
|
case RELATED = "related";
|
||||||
|
case PIVOT = "pivot";
|
||||||
|
}
|
10
src/Events/AfterDelete.php
Normal file
10
src/Events/AfterDelete.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity delete.
|
||||||
|
*/
|
||||||
|
class AfterDelete extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/AfterInsert.php
Normal file
10
src/Events/AfterInsert.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity insert.
|
||||||
|
*/
|
||||||
|
class AfterInsert extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
21
src/Events/AfterLoad.php
Normal file
21
src/Events/AfterLoad.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
use Nest\Model\Entity;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity relations load.
|
||||||
|
*/
|
||||||
|
class AfterLoad extends EntityEvent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Entity $entity The entity on which the event is fired.
|
||||||
|
* @param array<string, callable(EntityQuery): void|true> $relations Loaded relations.
|
||||||
|
*/
|
||||||
|
public function __construct(Entity $entity, public readonly array $relations)
|
||||||
|
{
|
||||||
|
parent::__construct($entity);
|
||||||
|
}
|
||||||
|
}
|
10
src/Events/AfterPropertiesInitialization.php
Normal file
10
src/Events/AfterPropertiesInitialization.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity properties initialization.
|
||||||
|
*/
|
||||||
|
class AfterPropertiesInitialization extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/AfterSave.php
Normal file
10
src/Events/AfterSave.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity save.
|
||||||
|
*/
|
||||||
|
class AfterSave extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/AfterUpdate.php
Normal file
10
src/Events/AfterUpdate.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired after entity update.
|
||||||
|
*/
|
||||||
|
class AfterUpdate extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/BeforeDelete.php
Normal file
10
src/Events/BeforeDelete.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity delete.
|
||||||
|
*/
|
||||||
|
class BeforeDelete extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/BeforeInsert.php
Normal file
10
src/Events/BeforeInsert.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity insert.
|
||||||
|
*/
|
||||||
|
class BeforeInsert extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
21
src/Events/BeforeLoad.php
Normal file
21
src/Events/BeforeLoad.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
use Nest\Model\Entity;
|
||||||
|
use Nest\Model\Query\EntityQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity relations load.
|
||||||
|
*/
|
||||||
|
class BeforeLoad extends EntityEvent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Entity $entity The entity on which the event is fired.
|
||||||
|
* @param array<string, callable(EntityQuery): void|true> $relations Relations to load.
|
||||||
|
*/
|
||||||
|
public function __construct(Entity $entity, public readonly array $relations)
|
||||||
|
{
|
||||||
|
parent::__construct($entity);
|
||||||
|
}
|
||||||
|
}
|
10
src/Events/BeforePropertiesInitialization.php
Normal file
10
src/Events/BeforePropertiesInitialization.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity properties initialization.
|
||||||
|
*/
|
||||||
|
class BeforePropertiesInitialization extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/BeforeSave.php
Normal file
10
src/Events/BeforeSave.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity save.
|
||||||
|
*/
|
||||||
|
class BeforeSave extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/BeforeUpdate.php
Normal file
10
src/Events/BeforeUpdate.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired before entity update.
|
||||||
|
*/
|
||||||
|
class BeforeUpdate extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
10
src/Events/EntityDefinitionEvent.php
Normal file
10
src/Events/EntityDefinitionEvent.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired when the entity definition function has been called.
|
||||||
|
*/
|
||||||
|
class EntityDefinitionEvent extends EntityEvent
|
||||||
|
{
|
||||||
|
}
|
17
src/Events/EntityEvent.php
Normal file
17
src/Events/EntityEvent.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
use Nest\Model\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any entity event.
|
||||||
|
*/
|
||||||
|
abstract class EntityEvent extends ModelEvent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Entity $entity The entity on which the event is fired.
|
||||||
|
*/
|
||||||
|
public function __construct(public readonly Entity $entity)
|
||||||
|
{}
|
||||||
|
}
|
12
src/Events/ModelEvent.php
Normal file
12
src/Events/ModelEvent.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Events;
|
||||||
|
|
||||||
|
use Nest\Events\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any model event.
|
||||||
|
*/
|
||||||
|
abstract class ModelEvent extends Event
|
||||||
|
{
|
||||||
|
}
|
21
src/Exceptions/MissingRequiredFieldException.php
Normal file
21
src/Exceptions/MissingRequiredFieldException.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Exceptions;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a required field of a model is missing.
|
||||||
|
*/
|
||||||
|
class MissingRequiredFieldException extends ModelException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string[] $missingFields Names of required fields that are missing.
|
||||||
|
* @param int $code [optional] The Exception code.
|
||||||
|
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
|
||||||
|
*/
|
||||||
|
public function __construct(public array $missingFields, int $code = 0, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct("The following fields are required: ".implode(", ", $this->missingFields), $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
9
src/Exceptions/ModelException.php
Normal file
9
src/Exceptions/ModelException.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Exceptions;
|
||||||
|
|
||||||
|
use Nest\Exceptions\Exception;
|
||||||
|
|
||||||
|
class ModelException extends Exception
|
||||||
|
{
|
||||||
|
}
|
22
src/Exceptions/UndefinedRelationException.php
Normal file
22
src/Exceptions/UndefinedRelationException.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Exceptions;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when a requested relation is undefined.
|
||||||
|
*/
|
||||||
|
class UndefinedRelationException extends ModelException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $modelClass Class of the model where the relation is undefined.
|
||||||
|
* @param string $relationName Name of the undefined relation.
|
||||||
|
* @param int $code [optional] The Exception code.
|
||||||
|
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
|
||||||
|
*/
|
||||||
|
public function __construct(public readonly string $modelClass, public readonly string $relationName, int $code = 0, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct("Undefined relation $this->relationName in $this->modelClass.", $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
22
src/Exceptions/UnhandledPropertyTypeException.php
Normal file
22
src/Exceptions/UnhandledPropertyTypeException.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Exceptions;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a property with an unhandled type has been provided.
|
||||||
|
*/
|
||||||
|
class UnhandledPropertyTypeException extends ModelException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $propertyName Name of the provided property.
|
||||||
|
* @param class-string $propertyType Class name of the unhandled property type.
|
||||||
|
* @param int $code [optional] The Exception code.
|
||||||
|
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
|
||||||
|
*/
|
||||||
|
public function __construct(public readonly string $propertyName, public readonly string $propertyType, int $code = 0, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct("Unhandled property $this->propertyName of type $this->propertyType.", $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
77
src/FieldBlueprint.php
Normal file
77
src/FieldBlueprint.php
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
abstract class FieldBlueprint
|
||||||
|
{
|
||||||
|
protected string $type;
|
||||||
|
|
||||||
|
protected bool $primary = false;
|
||||||
|
|
||||||
|
protected bool $index = false;
|
||||||
|
|
||||||
|
protected bool $unique = false;
|
||||||
|
|
||||||
|
protected bool $required = false;
|
||||||
|
|
||||||
|
protected bool $autoIncrement = false;
|
||||||
|
|
||||||
|
protected bool $unsigned = false;
|
||||||
|
|
||||||
|
protected bool $currentDateByDefault = false;
|
||||||
|
protected bool $currentDateOnUpdate = false;
|
||||||
|
|
||||||
|
public function type(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function primary(bool $primary = true): static
|
||||||
|
{
|
||||||
|
$this->primary = $primary;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(bool $index = true): static
|
||||||
|
{
|
||||||
|
$this->index = $index;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unique(bool $unique = true): static
|
||||||
|
{
|
||||||
|
$this->unique = $unique;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function required(bool $required = true): static
|
||||||
|
{
|
||||||
|
$this->required = $required;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function autoIncrement(bool $autoIncrement = true): static
|
||||||
|
{
|
||||||
|
$this->autoIncrement = $autoIncrement;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unsigned(bool $unsigned = true): static
|
||||||
|
{
|
||||||
|
$this->unsigned = $unsigned;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentDateByDefault(bool $currentDateByDefault = true): static
|
||||||
|
{
|
||||||
|
$this->currentDateByDefault = $currentDateByDefault;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentDateOnUpdate(bool $currentDateOnUpdate = true): static
|
||||||
|
{
|
||||||
|
$this->currentDateOnUpdate = $currentDateOnUpdate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
344
src/Query/EntityQuery.php
Normal file
344
src/Query/EntityQuery.php
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model\Query;
|
||||||
|
|
||||||
|
use Nest\Database\Database;
|
||||||
|
use Nest\Database\Query\SelectQuery;
|
||||||
|
use Nest\Database\Exceptions\Query\MissingConditionValueException;
|
||||||
|
use Nest\Database\Exceptions\UnknownDatabaseException;
|
||||||
|
use Nest\Exceptions\InvalidTypeException;
|
||||||
|
use Nest\Model\Exceptions\UndefinedRelationException;
|
||||||
|
use Nest\Model\Exceptions\UnhandledPropertyTypeException;
|
||||||
|
use Nest\Types\Exceptions\IncompatibleTypeException;
|
||||||
|
use Nest\Model\Entity;
|
||||||
|
use function Nest\Utils\array_first;
|
||||||
|
use function Nest\Utils\array_first_or_val;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query builder for an entity.
|
||||||
|
* @template Entity
|
||||||
|
*/
|
||||||
|
class EntityQuery extends SelectQuery
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Normalize a relations definition array.
|
||||||
|
* @param array<string|int, string|callable(EntityQuery): void> $relations Relations to normalize.
|
||||||
|
* @return array<string, (callable(EntityQuery): void)|true> Relations definition.
|
||||||
|
*/
|
||||||
|
public static function normalizeRelationsDefinition(array $relations): array
|
||||||
|
{
|
||||||
|
// Normalize relations array.
|
||||||
|
foreach ($relations as $key => $relation)
|
||||||
|
{ // Add each relation to eager load to the with array.
|
||||||
|
if (!is_string($key))
|
||||||
|
{ // If key is an integer, then $relation is a string key.
|
||||||
|
$relations[$relation] = true;
|
||||||
|
unset($relations[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return normalized relations array.
|
||||||
|
return $relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relations that should be eager loaded.
|
||||||
|
* @var array<string, (callable(EntityQuery): void)|true>
|
||||||
|
*/
|
||||||
|
protected array $with = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
* @param Entity $entity Reference entity.
|
||||||
|
*/
|
||||||
|
public function __construct(Database $database, string $table, protected Entity $entity)
|
||||||
|
{
|
||||||
|
parent::__construct($database, $table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new entity instance.
|
||||||
|
* @return Entity Entity instance.
|
||||||
|
*/
|
||||||
|
protected function newEntity(): Entity
|
||||||
|
{
|
||||||
|
// Create a new entity instance.
|
||||||
|
return $this->entity->new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queried entity primary key name.
|
||||||
|
* @return string Primary key name.
|
||||||
|
*/
|
||||||
|
public function getPrimaryKeyName(): string
|
||||||
|
{
|
||||||
|
return array_first_or_val($this->entity->getPrimaryFields());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a where condition for the primary key of the entity.
|
||||||
|
* @param mixed|array $keyValue Entity primary key value(s) to match.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function whereKey(mixed $keyValue): static
|
||||||
|
{
|
||||||
|
return $this->whereIn($this->getPrimaryKeyName(), is_array($keyValue) ? $keyValue : [$keyValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a relation prefix for the given relation.
|
||||||
|
* @param string $relationKey Relation key.
|
||||||
|
* @return string Relation prefix.
|
||||||
|
*/
|
||||||
|
private function getRelationPrefix(string $relationKey): string
|
||||||
|
{
|
||||||
|
return "{$this->entity->getTableName()}_{$relationKey}_";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through all first-level relations to eager load and add joins and selected columns for them.
|
||||||
|
* @param bool $generateSelect Set if SELECTed columns must be generated or not.
|
||||||
|
* @return string[] Loaded inline relations.
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
*/
|
||||||
|
private function addInlineRelations(bool $generateSelect): array
|
||||||
|
{
|
||||||
|
if (empty($this->with)) return [];
|
||||||
|
|
||||||
|
// Initialize list of inline loaded relations.
|
||||||
|
$inlineRelations = [];
|
||||||
|
|
||||||
|
foreach ($this->with as $relationKey => $relationCallable)
|
||||||
|
{ // For each relation, if it's inline, append a JOIN and SELECT for it.
|
||||||
|
// Get the required relation.
|
||||||
|
$relation = $this->entity->getRelation($relationKey);
|
||||||
|
if ($relation->canInlineLoad())
|
||||||
|
{ // If the relation is not multiple, it can be retrieved inline.
|
||||||
|
$relation->setupInlineLoading($this);
|
||||||
|
|
||||||
|
if ($generateSelect)
|
||||||
|
// Generate SELECT for the relation.
|
||||||
|
$this->select(...$relation->newEntity()->sqlSelectFields($this->getRelationPrefix($relationKey)));
|
||||||
|
|
||||||
|
if (is_callable($relationCallable))
|
||||||
|
// Call the callable with the alterable query.
|
||||||
|
$relationCallable($this);
|
||||||
|
|
||||||
|
// Add to inline loaded relations.
|
||||||
|
$inlineRelations[] = $relationKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $inlineRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queried entities.
|
||||||
|
* @param string|null $indexProperty A property to use as entities array index.
|
||||||
|
* @return Entity[]|array<mixed, Entity[]> Retrieved entities.
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
*/
|
||||||
|
public function get(?string $indexProperty = null): array
|
||||||
|
{
|
||||||
|
// If there's no custom SELECT, generate it.
|
||||||
|
$generateSelect = empty($this->selected);
|
||||||
|
|
||||||
|
if ($generateSelect)
|
||||||
|
// Generate SELECT for the main entity.
|
||||||
|
$this->select(...$this->entity->sqlSelectFields());
|
||||||
|
|
||||||
|
// Add inline relations retrieval to the query.
|
||||||
|
$inlineRelations = $this->addInlineRelations($generateSelect);
|
||||||
|
|
||||||
|
// Map entities with their inline relations, if there are some.
|
||||||
|
$entities = $this->mapEntities($this->execute(), $inlineRelations, $indexProperty);
|
||||||
|
|
||||||
|
// Load all remaining relations (remove already loaded inline relations).
|
||||||
|
$relationsToLoad = $this->with ?? [];
|
||||||
|
foreach ($inlineRelations as $inlineRelation)
|
||||||
|
unset($relationsToLoad[$inlineRelation]);
|
||||||
|
|
||||||
|
$this->load($entities, $relationsToLoad);
|
||||||
|
|
||||||
|
return $entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the entities with the given keys.
|
||||||
|
* @param mixed[] $keysValues Entity primary keys values to match.
|
||||||
|
* @return Entity[] Retrieved entities.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function findMany(array $keysValues): array
|
||||||
|
{
|
||||||
|
return $this->whereKey($keysValues)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the first entity of the query.
|
||||||
|
* @return Entity Retrieved entity.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function first(): Entity
|
||||||
|
{
|
||||||
|
return array_first($this->limit(1)->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the entity with the given key.
|
||||||
|
* @param mixed $keyValue Primary key value of the entity.
|
||||||
|
* @return Entity Retrieved entity.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
*/
|
||||||
|
public function find(mixed $keyValue): Entity
|
||||||
|
{
|
||||||
|
return $this->whereKey($keyValue)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map raw objects results to entities.
|
||||||
|
* @param object[] $objects Raw objects results.
|
||||||
|
* @param string[] $inlineRelations Inline relations to map, if there are some.
|
||||||
|
* @return Entity[] The entities.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
*/
|
||||||
|
protected function mapEntities(array $objects, array $inlineRelations = [], ?string $indexProperty = null): array
|
||||||
|
{
|
||||||
|
// Initialize entities array.
|
||||||
|
$entities = [];
|
||||||
|
|
||||||
|
foreach ($objects as $object)
|
||||||
|
{ // Map each object to an entity.
|
||||||
|
$entity = $this->mapEntity($object);
|
||||||
|
|
||||||
|
// Append to entities array, depending on index property name.
|
||||||
|
if (empty($indexProperty))
|
||||||
|
$entities[] = $entity;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (empty($object->$indexProperty)) $object->$indexProperty = [];
|
||||||
|
$entities[$object->$indexProperty][] = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map inline relations.
|
||||||
|
foreach ($inlineRelations as $inlineRelation)
|
||||||
|
{ // For each inline relation, map its data.
|
||||||
|
$relation = $this->entity->getRelation($inlineRelation);
|
||||||
|
// Create a new entity as property value.
|
||||||
|
$propertyValue = $relation->newEntity();
|
||||||
|
$propertyValue->fromSqlProperties($object, $this->getRelationPrefix($inlineRelation));
|
||||||
|
|
||||||
|
// Set the relation property.
|
||||||
|
$entity->setOriginalProperty($inlineRelation, $propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return parsed entities.
|
||||||
|
return $entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map raw object result to an entity.
|
||||||
|
* @param object $object Raw object.
|
||||||
|
* @return Entity The entity.
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
*/
|
||||||
|
protected function mapEntity(object $object): Entity
|
||||||
|
{
|
||||||
|
// Create a new entity.
|
||||||
|
$entity = $this->newEntity();
|
||||||
|
|
||||||
|
// Read entity from SQL properties.
|
||||||
|
$entity->fromSqlProperties($object);
|
||||||
|
|
||||||
|
// Return parsed entity.
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eager load given relations.
|
||||||
|
* @param Entity[] $entities Entities for which to load relations.
|
||||||
|
* @param array<string, callable(EntityQuery): void|true> $relationsToLoad Relations to load.
|
||||||
|
* @return void
|
||||||
|
* @throws IncompatibleTypeException
|
||||||
|
* @throws InvalidTypeException
|
||||||
|
* @throws MissingConditionValueException
|
||||||
|
* @throws UndefinedRelationException
|
||||||
|
* @throws UnknownDatabaseException
|
||||||
|
* @throws UnhandledPropertyTypeException
|
||||||
|
*/
|
||||||
|
public function load(array $entities, array $relationsToLoad): void
|
||||||
|
{
|
||||||
|
foreach ($relationsToLoad as $relationKey => $relationCallable)
|
||||||
|
{ // For each relation to load...
|
||||||
|
// Get relation instance.
|
||||||
|
$relation = $this->entity->getRelation($relationKey);
|
||||||
|
|
||||||
|
// Build base query to retrieve relations.
|
||||||
|
$query = $relation->queryFor($entities);
|
||||||
|
|
||||||
|
// Apply query modifier callable.
|
||||||
|
if (is_callable($relationCallable))
|
||||||
|
$relationCallable($query);
|
||||||
|
|
||||||
|
// Retrieve models and associate them with the right entities.
|
||||||
|
$models = $query->get("__reference_key");
|
||||||
|
|
||||||
|
foreach ($entities as &$entity)
|
||||||
|
{ // Associate the retrieved model(s) to every entity.
|
||||||
|
if (!empty($models[$entityId = $entity->{array_first_or_val($entity->getPrimaryFields())}]))
|
||||||
|
$entity->setOriginalProperty($relationKey, $relation->isMultiple() ? $models[$entityId] : array_first($models[$entityId]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add relations to eager load.
|
||||||
|
* @param array<string|int, string|callable(EntityQuery): void> $relations Relations that should be eager loaded.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function with(array $relations): static
|
||||||
|
{
|
||||||
|
// Normalize relations array and add them in the with array.
|
||||||
|
array_merge($this->with, EntityQuery::normalizeRelationsDefinition($relations));
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add relations to NOT eager load.
|
||||||
|
* @param string[] $relations Relations that shouldn't be eager loaded.
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function without(array $relations): static
|
||||||
|
{
|
||||||
|
foreach ($relations as $relation)
|
||||||
|
{ // For each relation to NOT eager load, remove it from the with array.
|
||||||
|
if (!empty($this->with[$relation])) unset($this->with[$relation]);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
37
src/ReadableArrayBlueprint.php
Normal file
37
src/ReadableArrayBlueprint.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Types\Type;
|
||||||
|
|
||||||
|
class ReadableArrayBlueprint extends ArrayBlueprint
|
||||||
|
{
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get type instance of the given field.
|
||||||
|
* @return Type Type instance.
|
||||||
|
*/
|
||||||
|
public function getTypeInstance(): Type
|
||||||
|
{
|
||||||
|
return new $this->type();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTable(): ?string
|
||||||
|
{
|
||||||
|
return $this->table;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getForeignKeyName(): ?string
|
||||||
|
{
|
||||||
|
return $this->foreignKeyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getForeignValueName(): ?string
|
||||||
|
{
|
||||||
|
return $this->foreignValueName;
|
||||||
|
}
|
||||||
|
}
|
59
src/ReadableEntityPropertyBlueprint.php
Normal file
59
src/ReadableEntityPropertyBlueprint.php
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
class ReadableEntityPropertyBlueprint extends EntityPropertyBlueprint
|
||||||
|
{
|
||||||
|
public function getClass(): string
|
||||||
|
{
|
||||||
|
return $this->class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMultiple(): bool
|
||||||
|
{
|
||||||
|
return $this->multiple;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInlineAllowed(): bool
|
||||||
|
{
|
||||||
|
return $this->allowInline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMode(): EntityPropertyMode
|
||||||
|
{
|
||||||
|
return $this->mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocalKey(): ?string
|
||||||
|
{
|
||||||
|
return $this->localKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelatedKey(): ?string
|
||||||
|
{
|
||||||
|
return $this->relatedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPivotTable(): ?string
|
||||||
|
{
|
||||||
|
return $this->pivotTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPivotLocalKey(): ?string
|
||||||
|
{
|
||||||
|
return $this->pivotLocalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPivotRelatedKey(): ?string
|
||||||
|
{
|
||||||
|
return $this->pivotRelatedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool True if the field should be eager loaded.
|
||||||
|
*/
|
||||||
|
public function doEagerLoad(): bool
|
||||||
|
{
|
||||||
|
return $this->eagerLoad;
|
||||||
|
}
|
||||||
|
}
|
62
src/ReadableFieldBlueprint.php
Normal file
62
src/ReadableFieldBlueprint.php
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Nest\Model;
|
||||||
|
|
||||||
|
use Nest\Types\Type;
|
||||||
|
|
||||||
|
class ReadableFieldBlueprint extends FieldBlueprint
|
||||||
|
{
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get type instance of the given field.
|
||||||
|
* @return Type Type instance.
|
||||||
|
*/
|
||||||
|
public function getTypeInstance(): Type
|
||||||
|
{
|
||||||
|
return new $this->type();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPrimary(): bool
|
||||||
|
{
|
||||||
|
return $this->primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isIndexed(): bool
|
||||||
|
{
|
||||||
|
return $this->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUnique(): bool
|
||||||
|
{
|
||||||
|
return $this->unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRequired(): bool
|
||||||
|
{
|
||||||
|
return $this->required;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAutoIncrementing(): bool
|
||||||
|
{
|
||||||
|
return $this->autoIncrement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUnsigned(): bool
|
||||||
|
{
|
||||||
|
return $this->unsigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCurrentDateByDefault(): bool
|
||||||
|
{
|
||||||
|
return $this->currentDateByDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCurrentDateOnUpdate(): bool
|
||||||
|
{
|
||||||
|
return $this->currentDateOnUpdate;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue