9 Commits
17 changed files with 304 additions and 184 deletions

No files matched your search

+3 -3
View File
@@ -10,12 +10,12 @@
}
],
"require": {
"mcnd/notes": "dev-master"
"mcnd/notes": "^2.1.1"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/mcNdave/notes.git"
"type": "composer",
"url": "https://git.mcnd.ca/api/packages/mcndave/composer"
}
],
"autoload": {
+1 -1
View File
@@ -7,7 +7,7 @@ use Ulmus\Repository;
class Attribute
{
public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
public static function handleArrayField(null|\Stringable|string|array $field, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
{
if ( is_array($field) ) {
if (count($field) < 2) {
+1 -1
View File
@@ -17,7 +17,7 @@ class Where {
public null|array|\Closure $generateValue = null,
) {
$this->field = Attribute::handleArrayField($field);
$this->fieldValue = Attribute::handleArrayField($fieldValue);
$this->fieldValue = Attribute::handleArrayField($fieldValue, false);
}
public function getValue(/* null|EntityInterface */ $entity = null) : mixed
+1 -1
View File
@@ -59,7 +59,7 @@ trait CacheTrait
$this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Update {
use Cache\CacheEventTrait;
public function execute(RepositoryInterface $repository, object|array $entity, ?array $dataset = null, bool $replace = false): void
public function execute(RepositoryInterface $repository, object|array $entity, array $dataset, array $diff, bool $replace = false): void
{
$this->purgeCache();
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Entity\Field;
class Date extends DatetimeImmutable {
public string $format = "Y-m-d";
}
+2 -63
View File
@@ -5,66 +5,5 @@ namespace Ulmus\Entity\Field;
use Ulmus\Entity\EntityObjectInterface;
class Datetime extends \DateTime implements EntityObjectInterface, \JsonSerializable {
public string $format = "Y-m-d H:i:s";
public function load(mixed ...$arguments)
{
$value = $arguments[0];
try {
# From Timestamp
if ( is_numeric($value) ) {
$obj = new static("@$value");
}
else {
$obj = new static($value);
}
}
catch(\Throwable $ex) {
throw new \Exception(sprintf("An error occured trying to instanciate from '%s'. %s", $value, $ex->getMessage()), $ex->getCode(), $ex);
}
return $obj;
}
public function setTime($hour, $minute, $second = 0, $microsecond = 0) : \DateTime
{
$fromParent = parent::setTime($hour, $minute, $second, $microsecond);
return new static($fromParent);
}
public function save()
{
return $this->__toString();
}
public function __toString()
{
return $this->format($this->format);
}
#[\Deprecated]
public function formatLocale(string $format) : string
{
return strftime($format, $this->getTimestamp());
}
public function formatLocaleIntl(int $dateFormatter = \IntlDateFormatter::LONG, int $timeFormatter = \IntlDateFormatter::NONE, ? string $pattern = null) : string
{
$formatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormatter, $timeFormatter, \date_default_timezone_get(), \IntlDateFormatter::GREGORIAN, $pattern);
return $formatter->format($this->getTimestamp());
}
public function ageAt(null|\DateTime $dateTime) : int
{
return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000;
}
public function jsonSerialize(): mixed
{
return $this;
}
}
use DatetimeTrait;
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Ulmus\Entity\Field;
use Ulmus\Entity\EntityObjectInterface;
class DatetimeImmutable extends \DateTimeImmutable implements EntityObjectInterface {
use DatetimeTrait;
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace Ulmus\Entity\Field;
trait DatetimeTrait
{
public string $format = "Y-m-d H:i:s";
public function load(mixed ...$arguments)
{
$value = $arguments[0];
try {
# From Timestamp
if ( is_numeric($value) ) {
$obj = new static("@$value");
}
else {
$obj = new static($value);
}
}
catch(\Throwable $ex) {
throw new \Exception(sprintf("An error occured trying to instanciate from '%s'. %s", $value, $ex->getMessage()), $ex->getCode(), $ex);
}
return $obj;
}
public function setTime($hour, $minute, $second = 0, $microsecond = 0) : \DateTime
{
$fromParent = parent::setTime($hour, $minute, $second, $microsecond);
return new static($fromParent);
}
public function save()
{
return $this->__toString();
}
public function __toString()
{
return $this->format($this->format);
}
#[\Deprecated]
public function formatLocale(string $format) : string
{
return strftime($format, $this->getTimestamp());
}
public function formatLocaleIntl(int $dateFormatter = \IntlDateFormatter::LONG, int $timeFormatter = \IntlDateFormatter::NONE, ? string $pattern = null) : string
{
$formatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormatter, $timeFormatter, \date_default_timezone_get(), \IntlDateFormatter::GREGORIAN, $pattern);
return $formatter->format($this->getTimestamp());
}
public function ageAt(null|\DateTime $dateTime) : int
{
return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000;
}
public function jsonSerialize(): mixed
{
return $this;
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Entity\Field;
class Time extends DatetimeImmutable {
public string $format = "H:i:s";
}
+14 -1
View File
@@ -93,7 +93,7 @@ trait EntityTrait {
$this->$field = $value;
}
$this->eventExecute(Event\Entity\DatasetFill::class, $this);
$this->eventExecute(Event\Entity\DatasetFill::class, $generator, $this);
$this->entityDatasetUnmatchedFields = $generator->getReturn();
@@ -268,6 +268,19 @@ trait EntityTrait {
}
}
#[Ignore]
public function __debugInfo() : array
{
try {
return $this->toArray(true) ;
}
catch (\Throwable $ex) {
return $this->entityGetDataset(false, true);
}
return [ "An error occured trying to export this dataset..." ];
}
#[Ignore]
public function jsonSerialize() : mixed
{
+1 -1
View File
@@ -5,5 +5,5 @@ namespace Ulmus\Event\Query;
use Ulmus\Repository\RepositoryInterface;
interface Update {
public function execute(RepositoryInterface $repository, object|array $entity, ? array $dataset = null, bool $replace = false) : void;
public function execute(RepositoryInterface $repository, object|array $entity, array $dataset, array $diff, bool $replace = false) : void;
}
+4
View File
@@ -70,6 +70,10 @@ class FieldDefinition {
$default = $this->getDefault();
$syntax = $this->adapter->tableSyntax();
if (is_callable($default)) {
$default = "(" . call_user_func($default) . ")";
}
return implode(' ', array_filter([
$this->isUnsigned() ? $syntax['unsigned'] : null,
$this->nullable ? "NULL" : "NOT NULL",
+1 -1
View File
@@ -106,7 +106,7 @@ class Having extends Fragment {
}
# whitelisting operators
return in_array(strtoupper($this->operator), [ '=', '!=', '<>', 'LIKE' ]) ? $this->operator : "=";
return in_array(strtoupper($this->operator), [ '=', '!=', '>', '>=', '<', '<=', '<>', 'LIKE', 'IS', 'IS NOT', 'REGEXP', 'IN' ]) ? $this->operator : "=";
}
protected function value()
+3 -108
View File
@@ -8,12 +8,13 @@ use Ulmus\Attribute\Property\{
use Psr\SimpleCache\CacheInterface;
use Ulmus\Common\EntityResolver;
use Ulmus\Entity\EntityInterface;
use Ulmus\Repository\JoinBuilder;
use Ulmus\Repository\RepositoryInterface;
use Ulmus\Repository\WithOptionEnum;
class Repository implements RepositoryInterface
{
use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait;
use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait, Repository\JoinBuilderTrait;
const DEFAULT_ALIAS = "this";
@@ -263,7 +264,7 @@ class Repository implements RepositoryInterface
$entity->entityFillFromDataset($dataset, true);
$this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace);
$this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $diff, $replace);
return $update ? (bool) $update->rowCount : false;
}
@@ -450,112 +451,6 @@ class Repository implements RepositoryInterface
return $this->where($this->entityClass::field($primaryKeyField[$pkField]->name ?? $pkField, $alias), $value);
}
public function withJoin(string|array $fields, array $options = []) : self
{
$canSelect = null === $this->queryBuilder->getFragment(Query\Select::class);
if ( $canSelect ) {
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true));
}
$fields = (array) $fields;
usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.'));
# @TODO Apply fff annotation to this too !
foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) {
$this->joined[$item] = true;
$attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]));
$isRelation = $attribute instanceof Relation;
if ($isRelation && ( $attribute->isManyToMany() )) {
throw new \Exception("Many-to-many relation can not be preloaded within joins.");
}
if ( $attribute ) {
$alias = $attribute->alias ?? $item;
$entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item);
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
$escAlias = $this->escapeIdentifier($alias);
$fieldName = $this->escapeIdentifier($key);
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
if ($canSelect) {
# @TODO add handling of the new #[Select] attribute here !
$this->select("$escAlias.$fieldName as $alias\${$name}");
}
}
}
$this->open();
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) {
$this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
}
}
}
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
$this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
}
}
if ( ! in_array(WithOptionEnum::SkipFilter, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) {
call_user_func_array([$this->entityClass, $filter->method], [$this, $item, true]);
}
}
$this->close();
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
$foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey;
$this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) {
if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
if ( ! is_object($condition->field) ) {
$field = $this->entityClass::field($condition->field);
}
else {
$field = clone $condition->field;
}
# Adding directly
if ( $field->entityClass === $entity ) {
$field->alias = $alias;
$join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->getValue(), $condition->operator);
}
}
}
if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) {
call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]);
}
}
});
}
else {
throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute.");
}
}
return $this;
}
public function withSubquery(string|array $fields, array $options = []) : self
{
# We skip subqueries when counting results since it should not affect the row count.
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace Ulmus\Repository;
use Ulmus\Attribute\Property\Field;
use Ulmus\Attribute\Property\Filter;
use Ulmus\Attribute\Property\FilterJoin;
use Ulmus\Attribute\Property\Having;
use Ulmus\Attribute\Property\Join;
use Ulmus\Attribute\Property\Relation;
use Ulmus\Attribute\Property\Where;
use Ulmus\Common\EntityField;
use Ulmus\Common\EntityResolver;
use Ulmus\QueryBuilder\QueryBuilderInterface;
use Ulmus\Query;
trait JoinBuilderTrait
{
public function withJoin(string|array $fields, array $options = []) : self
{
$defaultAlias = $this->escapeIdentifier(static::DEFAULT_ALIAS);
$canSelect = null === $this->queryBuilder->getFragment(Query\Select::class);
if ( $canSelect ) {
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true));
}
$fields = (array) $fields;
usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.'));
# @TODO Apply fff annotation to this too !
foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) {
$this->joined[$item] = true;
$attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]));
$isRelation = $attribute instanceof Relation;
if ($isRelation && ( $attribute->isManyToMany() )) {
throw new \Exception("Many-to-many relation can not be preloaded within joins.");
}
if ( $attribute ) {
$alias = $attribute->alias ?? $item;
$entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item);
foreach($entity::resolveEntity()->fieldList(EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
$escAlias = $this->escapeIdentifier($alias);
$fieldName = $this->escapeIdentifier($key);
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
if ($canSelect) {
# @TODO add handling of the new #[Select] attribute here !
$this->select("$escAlias.$fieldName as $alias\${$name}");
}
}
}
$this->open();
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
$field = $this->joinFieldObject($condition->field, $this->entityClass);
if ( $field->entityClass === $this->entityClass ) {
$this->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
}
}
}
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
$field = $this->joinFieldObject($condition->field, $this->entityClass);
$this->having($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
}
}
if ( ! in_array(WithOptionEnum::SkipFilter, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) {
call_user_func_array([$this->entityClass, $filter->method], [ $this, $item, true ]);
}
}
$this->close();
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
$foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey;
$this->join(Query\Join::TYPE_LEFT, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $defaultAlias, $options) {
if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
$field = $this->joinFieldObject($condition->field, $this->entityClass);
# Adding directly
if ( $field->entityClass === $entity ) {
if ($field->alias === $defaultAlias) {
$field->alias = $this->escapeIdentifier($alias);
}
$join->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
}
}
}
if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) {
call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]);
}
}
});
}
else {
throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute.");
}
}
return $this;
}
protected function joinFieldObject(mixed $field, string $entityClass, string $alias = null) : \Stringable
{
if ( ! is_object($field) ) {
return $entityClass::field($field, $alias);
}
else {
return clone $field;
}
}
protected function joinFieldValue(mixed $value, string $alias = null) : mixed
{
if ( $value instanceof EntityField ) {
$value = clone $value;
$value->alias = $value->alias ?: $this->escapeIdentifier($alias);
return $value;
}
return $value;
}
}
+6 -4
View File
@@ -15,11 +15,13 @@ class ParsedSearchParameter
) {}
public function value() : mixed{
$value = $this->value instanceof \BackedEnum ? $this->value->value : $this->value;
return match ($this->searchMethod) {
SearchMethodEnum::Like => "%$this->value%",
SearchMethodEnum::LikeLeft => "%$this->value",
SearchMethodEnum::LikeRight => "$this->value%",
default => $this->value,
SearchMethodEnum::Like => "%$value%",
SearchMethodEnum::LikeLeft => "%$value",
SearchMethodEnum::LikeRight => "$value%",
default => $value,
};
}
@@ -190,7 +190,28 @@ trait SearchRequestFromRequestTrait
$this->$propertyName = !empty($value);
}
elseif ($value !== null) {
$this->$propertyName = $this->parseValue($property, $value);
/*=======
foreach ($property->getTypes() as $type) {
$enum = $type->type;
if (enum_exists($enum)) {
try {
$this->$propertyName = $value instanceof \UnitEnum ? $value : $type->type::from($value);
} catch (\ValueError $ex) {
$cases = implode(', ', array_map(fn($e) => $e->value, $enum::cases()));
throw new \ValueError(
sprintf("Given value '%s' do not exists within enum '%s'. Try one of those values instead : %s", $value, $enum, $cases)
);
}
}
elseif ($type->builtIn || $value instanceof \Stringable ) {
$this->$propertyName = $value;
}
}
>>>>>>> d917678 (- Moved withJoin() method of repository into a trait)*/
}
$this->parseAttributeMethod($attribute, $field, $propertyName);