ulmus/src/EntityTrait.php

373 lines
12 KiB
PHP

<?php
namespace Ulmus;
use Notes\Attribute\Ignore;
use Ulmus\{Attribute\Obj\JsonSerialize,
Attribute\Property\ResettablePropertyInterface,
Common\EntityResolver,
Common\EntityField,
Entity\DatasetHandler,
Entity\EntityInterface,
QueryBuilder\QueryBuilderInterface};
use Ulmus\{Attribute\RegisterEntityEventInterface, Entity\EntityValueModifier, };
#[JsonSerialize(includeRelations: true)]
trait EntityTrait {
use EventTrait;
#[Ignore]
public array $entityLoadedDataset = [];
#[Ignore]
protected bool $entityStrictFieldsDeclaration = false;
#[Ignore]
protected array $entityDatasetUnmatchedFields = [];
#[Ignore]
public DatasetHandler $datasetHandler;
#[Ignore]
public string $loadedFromAdapter;
#[Ignore]
public function __construct(iterable|null $dataset = null)
{
$this->initializeEntity($dataset);
}
#[Ignore]
public function initializeEntity(iterable|null $dataset = null) : void
{
# Attributs can attach themselves on events thrown by this trait
$this->entityRegisterAttributes();
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
if ($dataset) {
$this->fromArray($dataset);
}
$this->resetVirtualProperties();
}
#[Ignore]
public function entityRegisterAttributes() : void
{
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) {
if ( $tag->object instanceof RegisterEntityEventInterface ) {
$tag->object->attach($this, $field);
}
}
}
}
#[Ignore]
public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self
{
$properties = $this->resolveEntity()->reflectedClass->getProperties(true);
$loaded = $this->isLoaded();
$generator = $this->datasetHandler->push($dataset);
foreach($generator as $field => $value) {
foreach($properties[$field]->attributes as $tag) {
if ( $tag->object instanceof EntityValueModifier ) {
try {
$value = $tag->object->push($value, $properties[$field]);
}
catch(\Throwable $ex) {
throw new \LogicException(
message: sprintf("An error occured trying to push an EntityValueModifier results : %s", $ex->getMessage()),
code: $ex->getCode(),
previous: $ex,
);
}
}
}
$this->$field = $value;
}
$this->eventExecute(Event\Entity\DatasetFill::class, $this);
$this->entityDatasetUnmatchedFields = $generator->getReturn();
# Keeping original data to diff on UPDATE query
if ( ! $loaded ) {
$this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER);
}
elseif ($overwriteDataset) {
$this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER) + $this->entityLoadedDataset;
}
return $this;
}
#[Ignore]
public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false, bool $rewriteValue = true) : array
{
if ( $returnSource ) {
return $this->entityLoadedDataset;
}
return iterator_to_array($this->entityYieldDataset($includeRelations, $rewriteValue));
}
#[Ignore]
protected function entityYieldDataset(bool $includeRelations = false, bool $rewriteValue = true) : \Generator
{
foreach($this->datasetHandler->pull($this) as $field => $value) {
yield $field => $rewriteValue ? static::repository()->adapter->adapter()->writableValue($value) : $value;
}
if ($includeRelations) {
foreach($this->datasetHandler->pullRelation($this) as $field => $object) {
yield $field => $object;
}
}
}
#[Ignore]
public function entityGetDiffDataset() : array
{
$return = [];
foreach($this->datasetHandler->pull($this, false) as $field => $value) {
$return[$field] = static::repository()->adapter->adapter()->writableValue($value);
}
return $return;
}
#[Ignore]
public function resetVirtualProperties() : self
{
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) {
if ( $tag->object instanceof ResettablePropertyInterface ) {
unset($this->$field);
}
}
}
return $this;
}
#[Ignore]
public function fromArray(iterable|EntityInterface $dataset) : static
{
if ($dataset instanceof EntityInterface) {
$dataset = $dataset->toArray();
}
return $this->entityFillFromDataset($dataset);
}
#[Ignore]
public function toArray($includeRelations = false, array $filterFields = [], bool $rewriteValue = true) : array
{
$dataset = $this->entityGetDataset($includeRelations, false, $rewriteValue);
return $filterFields ? array_intersect_key($dataset, array_flip($filterFields)) : $dataset;
}
#[Ignore]
public function toCollection() : EntityCollection
{
return static::entityCollection([ $this ]);
}
#[Ignore]
public function isLoaded() : bool
{
if (empty($this->entityLoadedDataset)) {
return false;
}
if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField() ) {
if ( null !== $compoundKeyFields = $this->resolveEntity()->getCompoundKeyFields() ) {
$loaded = false;
foreach ($compoundKeyFields->column as $column) {
$field = $this->resolveEntity()->field($column);
if (! $field->allowsNull() ) {
$loaded |= isset($this->{$field->name});
}
}
return $loaded;
};
}
else {
$key = key($pkField);
return isset($this->$key);
}
throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class));
}
#[Ignore]
public function __get(string $name) : mixed
{
$relation = new Repository\RelationBuilder($this);
if ( false !== $data = $relation->searchRelation($name) ) {
return $this->$name = $data;
}
throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name));
}
#[Ignore]
public function __isset(string $name) : bool
{
$rel = static::resolveEntity()->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ]);
if ( $rel && $this->isLoaded() ) {
return true;
}
if (isset($this->$name)) {
return true;
}
# Unexisting variable
if (! new \ReflectionClass($this)->hasProperty($name)) {
return false;
}
# Handling undefined 'nullable'
return new \ReflectionProperty($this, $name)->isInitialized($this);
}
#[Ignore]
public function __sleep()
{
return array_merge(array_keys($this->resolveEntity()->fieldList()), [ 'entityLoadedDataset' ] );
}
#[Ignore]
public function __wakeup()
{
$this->resetVirtualProperties();
}
#[Ignore]
public function __clone()
{
if ( null !== $pkField = $this->resolveEntity()->getPrimaryKeyField($this) ) {
$key = key($pkField);
unset($this->$key);
}
}
#[Ignore]
public function jsonSerialize() : mixed
{
# Allows overridding of this method
return $this->_jsonSerialize();
}
#[Ignore]
public function _jsonSerialize() : mixed
{
$resolver = static::resolveEntity();
$objectAttribute = $resolver->getAttributeImplementing(JsonSerialize::class);
$dataset = [];
foreach($this->entityYieldDataset($objectAttribute->includeRelations ?? true, false, false) as $key => $value) {
$field = $resolver->searchField($key, EntityResolver::KEY_COLUMN_NAME); # ?: $resolver->relation($key, false);
if ($field) {
$jsonSerialize = $field->getAttribute(\Ulmus\Attribute\Property\JsonSerialize::class);
if ($jsonSerialize) {
# Should we ignore the field ?
if ($jsonSerialize->object->ignoreField) {
continue;
}
}
}
if (is_object($value) && ($value instanceof \JsonSerializable) ) {
$dataset[$key] = $value->jsonSerialize();
}
else {
$dataset[$key] = $value;
}
}
return $dataset;
}
#[Ignore]
public static function resolveEntity() : EntityResolver
{
return Ulmus::resolveEntity(static::class);
}
#[Ignore]
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ?ConnectionAdapter $adapter = null) : Repository
{
return Ulmus::repository(static::class, $alias, $adapter);
}
#[Ignore]
public static function entityCollection(...$arguments) : EntityCollection
{
$collection = new EntityCollection(...$arguments);
$collection->entityClass = static::class;
return $collection;
}
#[Ignore]
public static function queryBuilder() : QueryBuilderInterface
{
return Ulmus::queryBuilder(static::class);
}
#[Ignore]
public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS, bool $select = false) : EntityField
{
$default = ( $alias === false ? '' : static::repository()::DEFAULT_ALIAS ); # bw compatibility, to be deprecated
$alias = $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default;
return new EntityField(
name: $name, entityClass: static::class, alias: $alias, entityResolver: Ulmus::resolveEntity(static::class), select: $select
);
}
#[Ignore]
public static function fields(array $fields, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ', bool $select = false) : string
{
return implode($separator, array_map(fn($e) => static::field($e, $alias, $select), $fields));
}
#[Ignore]
public static function searchRequest(...$arguments) : SearchRequest\SearchRequestInterface
{
return new /* #[SearchRequest\Attribute\SearchRequestParameter(YourEntityClass::class)] */ class(... $arguments) extends SearchRequest\SearchRequest {
# Define searchable properties here, some ex:
# #[SearchParameter(method: SearchMethodEnum::Where)]
# public ? string $username = null;
# #[SearchParameter(method: SearchMethodEnum::Where, toggle: true)]
# public ? bool $hidden = null;
# #[SearchParameter(method: SearchMethodEnum::Like)]
# public ? string $word = null;
};
}
}