Compare commits

...

30 Commits

Author SHA1 Message Date
Dave Mc Nicoll
70c6cde603 - Latest commit into this branch, merging after 2024-11-13 14:02:03 -05:00
Dave Mc Nicoll
079a3fd110 Merge branch 'notes-2.x' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-11-11 20:42:51 +00:00
Dave Mc Nicoll
ec4d02b9d9 - Removed 'description' property and moved it into an appropriate attribute from lean-api package 2024-11-11 20:42:29 +00:00
Dave Mc Nicoll
35ceb97241 - Added Exists method 2024-11-10 13:08:05 +00:00
Dave Mc Nicoll
411992c7a8 - Added a new Field Mapper to SQLs Adapters.
- Defined missing MySQL fields attributes
- WIP on lean-console's migration methods
2024-10-21 18:12:24 +00:00
Dave Mc Nicoll
5f4f23a8e4 - Allowed a JOINed entity to be null if nothing is loaded from a query and it's field is nullable 2024-10-14 16:32:23 +00:00
Dave Mc Nicoll
2fda7e82d7 Merge branch 'notes-2.x' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-10-14 16:07:28 +00:00
Dave Mc Nicoll
573d4cc06b - RelationBuilder now returns NULL if no entity were found AND given field is nullable 2024-10-14 16:07:12 +00:00
Dave Mc Nicoll
18313fd7f5 - Forgot to set escapeIdentifier as static 2024-10-14 14:43:03 +00:00
Dave Mc Nicoll
9503b24467 - Fixed some bugs within SearchRequestFromRequestTrait 2024-10-14 14:41:35 +00:00
Dave Mc Nicoll
3c2ae86653 - Field mapper is now removed from Adapters and set into it's own object
- Added a PK to Column (IS)
2024-10-14 12:53:15 +00:00
Dave Mc Nicoll
9977a25cf5 - Forgot a dump() 2024-10-07 19:39:52 +00:00
Dave Mc Nicoll
3ba8a2a5d0 - Fixed 'value' for SearchRequestOrderBy and also fixed a typo where SearchManual would not be set 2024-10-07 19:38:29 +00:00
Dave Mc Nicoll
4106c8db91 Merge branch 'notes-2.x' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-10-07 19:37:10 +00:00
Dave Mc Nicoll
d5cca085bc - Removed offset if limit = 0 2024-10-07 19:36:53 +00:00
c56b584140 - Merges 2024-10-02 14:18:35 -04:00
Dave Mc Nicoll
8b1c11676d - Added OrderBy (heavy WIP, will be recoded 2024-08-30 18:59:48 +00:00
aa07c728fe - Fixed transactions in SQLite 2024-06-28 07:47:53 -04:00
Dave Mc Nicoll
d4181065cf Merge branch 'notes-2.x' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-06-28 11:46:46 +00:00
Dave Mc Nicoll
62998faf40 - WIP on SearchRequest enums and manual searchs 2024-06-28 11:46:09 +00:00
feee26cd26 Merge branch 'notes-2.x' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-06-21 13:01:48 +00:00
e37944c63a - Began WIP on SQL specific PdoObject 2024-06-21 13:01:21 +00:00
1d38ae8245 Merge branch 'master' of https://git.mcnd.ca/mcndave/ulmus into notes-2.x 2024-06-21 13:00:57 +00:00
Dave Mc Nicoll
0c8591f238 - Added multiple parameters on SearchParameter 2024-06-05 17:47:31 +00:00
Dave Mc Nicoll
bbfd7c02b4 - WIP on SearchAttributes supporting Request's attributes 2024-06-04 13:33:48 +00:00
Dave Mc Nicoll
bd9078230d - Multiple bug fixes related to notes 2.x 2024-05-31 12:26:39 +00:00
f3be11a590 - WIP on notes 2.x 2024-05-28 08:59:12 -04:00
Dave Mc Nicoll
2de3139c80 - Added description to most attributes 2024-05-27 18:09:22 +00:00
Dave Mc Nicoll
e37eeb85f1 Merge branch 'master' into notes-2.x 2024-05-21 14:01:50 +00:00
75231f32b3 - WIP on Notes v2.x 2024-05-09 19:49:27 +00:00
85 changed files with 1064 additions and 1116 deletions

View File

@ -2,7 +2,7 @@
namespace Ulmus\Adapter;
use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Entity, Migration\FieldDefinition, Repository, QueryBuilder\Sql\MysqlQueryBuilder};
use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder\Sql\MysqlQueryBuilder, Entity};
trait DefaultAdapterTrait
{

View File

@ -203,8 +203,8 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
$this->traceOn = $configuration['trace_on'];
}
}
public function escapeIdentifier(string $segment, int $type) : string
public static function escapeIdentifier(string $segment, int $type) : string
{
switch($type) {
default:

View File

@ -2,11 +2,15 @@
namespace Ulmus\Adapter;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\Table;
use Ulmus\Migration\FieldDefinition;
use Ulmus\Migration\MigrateInterface;
use Ulmus\QueryBuilder\Sql;
use Ulmus\Common\PdoObject;
use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Repository;
class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait;
@ -136,7 +140,14 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
}
}
public function escapeIdentifier(string $segment, int $type) : string
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new MySQLFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public static function escapeIdentifier(string $segment, int $type) : string
{
switch($type) {
case static::IDENTIFIER_DATABASE:
@ -159,4 +170,11 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
return Sql\MysqlQueryBuilder::class;
}
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
return Table::repository(Repository::DEFAULT_ALIAS, $adapter)
->select(\Ulmus\Common\Sql::raw('this.*'))
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Migration\FieldDefinition;
class MySQLFieldMapper extends SqlFieldMapper
{
public readonly string $type;
public readonly string $length;
public function map() : void
{
$type = $this->field->type;
$length = $this->field->length;
if ( enum_exists($type) ) {
# Haven't found a better way yet to check for BackendEnum without an instance of the object
if ( ! method_exists($type, 'tryFrom') ) {
throw new \Ulmus\Exception\BackedEnumRequired(sprintf("You must define your enum as a BackedEnum instead of an UnitEnum for field '%s'.", $this->field->getColumnName()));
}
$this->length = implode(',', array_map(fn($e) => MySQL::escapeIdentifier($e->value, MySQL::IDENTIFIER_VALUE) , $type::cases()));
$this->type = "ENUM";
}
else {
parent::map();
}
}
public function postProcess() : void
{
if (
in_array($this->type, [ 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'JSON', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'GEOMETRY' ]) &&
! is_object($this->field->default ?? false) # Could be a functional default, which would now be valid
) {
unset($this->field->default);
}
}
}

View File

@ -27,7 +27,7 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
public function connect() : PdoObject
{
try {
$pdo = new PdoObject($this->buildDataSourceName(), null, null);
$pdo = new PdoObject\SqlitePdoObject($this->buildDataSourceName(), null, null);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
@ -61,7 +61,7 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
}
# https://sqlite.org/lang_keywords.html
public function escapeIdentifier(string $segment, int $type) : string
public static function escapeIdentifier(string $segment, int $type) : string
{
switch($type) {
default:
@ -97,7 +97,6 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$type = $field->type;
$length = $field->length;
if ( is_a($type, Entity\Field\Date::class, true) || is_a($type, Entity\Field\Time::class, true) || is_a($type, \DateTime::class, true) ) {
@ -115,6 +114,10 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
break;
case "array":
$type = "JSON";
$length = null;
break;
case "string":
$type = "TEXT";
$length = null;
@ -130,7 +133,11 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
}
}
return $typeOnly ? $type : $type . ( $length ? "($length" . ( isset($precision) ? ",$precision" : "" ) . ")" : "" );
if (in_array($type, [ 'JSON', 'TEXT', 'BLOB', 'GEOMETRY' ])) {
$field->default = "";
}
return $typeOnly ? $type : $type . ( $length ? "($length" . ")" : "" );
}
public function tableSyntax() : array

View File

@ -4,6 +4,6 @@ namespace Ulmus\Adapter;
interface SqlAdapterInterface
{
public function escapeIdentifier(string $segment, int $type) : string;
public static function escapeIdentifier(string $segment, int $type) : string;
public function defaultEngine() : ? string;
}

View File

@ -46,60 +46,9 @@ trait SqlAdapterTrait
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$type = $field->type;
$mapper = new SqlFieldMapper($field);
$length = $field->length;
if ( is_a($type, Entity\Field\Date::class, true) ) {
$type = "DATE";
}
elseif ( is_a($type, Entity\Field\Time::class, true) ) {
$type = "TIME";
}
elseif ( is_a($type, \DateTime::class, true) ) {
$type = "DATETIME";
}
switch($type) {
case "bool":
$type = "TINYINT";
$length = 1;
break;
case "array":
case "string":
if ($length && $length <= 255) {
$type = "VARCHAR";
break;
}
elseif (! $length || ( $length <= 65535 ) ) {
$type = "TEXT";
}
elseif ( $length <= 16777215 ) {
$type = "MEDIUMTEXT";
}
elseif ($length <= 4294967295) {
$type = "LONGTEXT";
}
else {
throw new \Exception("A column with size bigger than 4GB cannot be created.");
}
# Length is unnecessary on TEXT fields
unset($length);
break;
case "float":
$type = "DOUBLE";
break;
default:
$type = strtoupper($type);
break;
}
return $typeOnly ? $type : $type . ( isset($length) ? "($length" . ( ! empty($precision) ? ",$precision" : "" ) . ")" : "" );
return $typeOnly ? $mapper->type : $mapper->render();
}
public function whitelistAttributes(array &$parameters) : void

View File

@ -0,0 +1,95 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Migration\FieldDefinition;
class SqlFieldMapper
{
public readonly string $type;
public readonly string $length;
public function __construct(
public FieldDefinition $field,
) {
$this->map();
}
public function map() : void
{
$type = $this->field->type;
$length = $this->field->length;
if ( is_a($type, Entity\Field\Date::class, true) ) {
$type = "DATE";
}
elseif ( is_a($type, Entity\Field\Time::class, true) ) {
$type = "TIME";
}
elseif ( is_a($type, \DateTime::class, true) ) {
$type = "DATETIME";
}
elseif ( enum_exists($type) ) {
# Haven't found a better way yet to check for BackendEnum without an instance of the object
if ( ! method_exists($type, 'tryFrom') ) {
throw new \Ulmus\Exception\BackedEnumRequired(sprintf("You must define your enum as a BackedEnum instead of an UnitEnum for field '%s'.", $this->field->getColumnName()));
}
$type = "string";
}
switch($type) {
case "bool":
$this->type = "TINYINT";
$length = 1;
break;
case "array":
$this->type = "JSON";
break;
case "string":
if ($length && $length <= 255) {
$this->type = "VARCHAR";
break;
}
elseif (! $length || ( $length <= 65535 ) ) {
$this->type = "TEXT";
}
elseif ( $length <= 16777215 ) {
$this->type = "MEDIUMTEXT";
}
elseif ($length <= 4294967295) {
$this->type = "LONGTEXT";
}
else {
throw new \Exception("A column with a length bigger than 4GB cannot be created.");
}
# Length is unnecessary on TEXT fields
unset($length);
break;
case "float":
$this->type = "DOUBLE";
break;
default:
$this->type = strtoupper($type);
break;
}
$this->postProcess();
}
public function render() : string
{
return $this->type . ( isset($this->length) ? "($this->length)" : "" );
}
public function postProcess() : void
{
}
}

View File

@ -1,7 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Collation implements \Notes\Annotation {
}

View File

@ -1,7 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Method implements \Notes\Annotation {
}

View File

@ -1,27 +0,0 @@
<?php
namespace Ulmus\Annotation\Classes;
class Table implements \Notes\Annotation {
public string $name;
public string $database;
public string $schema;
public string $adapter;
public string $engine;
public function __construct($name = null, $engine = null)
{
if ( $name !== null ) {
$this->name = $name;
}
if ( $engine !== null ) {
$this->engine = $engine;
}
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Field implements \Notes\Annotation {
public string $type;
public string $name;
public int|string $length;
public int $precision;
public array $attributes = [];
public bool $nullable;
public /* mixed */ $default;
public bool $readonly = false;
public function __construct(? string $type = null, ? int $length = null)
{
if ( $type !== null ) {
$this->type = $type;
}
if ( $length !== null ) {
$this->length = $length;
}
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Bigint extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "bigint", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Blob extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "blob", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class CreatedAt extends \Ulmus\Annotation\Property\Field {
public function __construct()
{
$this->nullable = false;
$this->type = "timestamp";
$this->attributes['default'] = "CURRENT_TIMESTAMP";
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Date extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "date", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Datetime extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "datetime", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
/**
* Since we need consistancy between the declaration of our ID and FK fields, it
* was decided to extend the Id class instead of Field for this case.
*/
class ForeignKey extends PrimaryKey {
public function __construct(? string $type = null, ? int $length = null)
{
parent::__construct($type, $length);
unset($this->nullable);
$this->attributes['primary_key'] = false;
$this->attributes['auto_increment'] = false;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Id extends \Ulmus\Annotation\Property\Field\PrimaryKey {
public function __construct()
{
$this->attributes['unsigned'] = true;
$this->attributes['auto_increment'] = true;
parent::__construct('bigint');
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Longblob extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "longblob", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Longtext extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "longtext", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Mediumblob extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "mediumblob", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Mediumtext extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "mediumtext", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class PrimaryKey extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = null, ? int $length = null)
{
$this->nullable = false;
$this->attributes['primary_key'] = true;
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Text extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "text", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Time extends \Ulmus\Annotation\Property\Field {
public function __construct(? string $type = "time", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Blob extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "tinyblob", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class Tinyint extends \Ulmus\Annotation\Property\Field
{
public function __construct(? string $type = "tinyint", ? int $length = null)
{
parent::__construct($type, $length);
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Field;
class UpdatedAt extends \Ulmus\Annotation\Property\Field {
public function __construct()
{
$this->nullable = true;
$this->type = "timestamp";
$this->attributes['update'] = "CURRENT_TIMESTAMP";
$this->attributes['default'] = null;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Filter implements \Notes\Annotation {
public string $method;
public function __construct(string $method = null)
{
if ( $method !== null ) {
$this->method = $method;
}
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class FilterJoin implements \Notes\Annotation {
public string $method;
public function __construct(string $method = null)
{
if ( $method !== null ) {
$this->method = $method;
}
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class GroupBy implements \Notes\Annotation {
public array $fields = [];
public function __construct(...$field)
{
if ( $field ) {
$this->fields = $field;
}
}
}

View File

@ -1,5 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Having extends Where {}

View File

@ -1,31 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Join implements \Notes\Annotation {
public string $type;
public string|Stringable $key;
public string|Stringable $foreignKey;
public string $entity;
public string $alias;
public function __construct(? string $type = null, /*? string|Stringable*/ $key = null, /*? string|Stringable*/ $foreignKey = null)
{
if ($type !== null) {
$this->type = $type;
}
if ($key !== null) {
$this->key = $key;
}
if ($foreignKey !== null) {
$this->foreignKey = $foreignKey;
}
}
}

View File

@ -1,5 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class On extends Where {}

View File

@ -1,14 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
use Ulmus\Query;
class OrWhere extends Where {
public function __construct(/* ? Stringable */ $field = null, $value = null, ? string $operator = null, ? string $condition = null)
{
parent::__construct($field, $value, $operator, Query\Where::CONDITION_OR);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class OrderBy implements \Notes\Annotation {
public string $field;
public string $order = "ASC";
public function __construct(string $field = null, string $order = null)
{
if ( $field !== null ) {
$this->field = $field;
}
if ( $order !== null ) {
$this->order = $order;
}
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Relation implements \Notes\Annotation {
public string $type;
public /*stringable*/ $key;
public /* callable */ $generateKey;
public /*stringable*/ $foreignKey;
public array $foreignKeys;
public string $bridge;
public /*stringable*/ $bridgeKey;
public /*stringable*/ $bridgeForeignKey;
public string $entity;
public string $join;
public string $function = "loadAll";
public function __construct(string $type = null)
{
if ( $type !== null ) {
$this->type = $type;
}
}
public function entity() {
try {
$e = $this->entity;
} catch (\Throwable $ex) {
throw new \Exception("Your @Relation annotation seems to be missing an `entity` entry.");
}
return new $e();
}
public function bridge() {
$e = $this->bridge;
return new $e();
}
public function normalizeType() : string
{
return strtolower(str_replace(['-', '_', ' '], '', $this->type));
}
public function isOneToOne() : bool
{
return $this->normalizeType() === 'onetoone';
}
public function isOneToMany() : bool
{
return $this->normalizeType() === 'onetomany';
}
public function isManyToMany() : bool
{
return $this->normalizeType() === 'manytomany';
}
public function function() : string
{
if ($this->function) {
return $this->function;
}
elseif ($this->isOneToOne()) {
return 'load';
}
return 'loadAll';
}
public function hasBridge() : bool
{
return isset($this->bridge);
}
}

View File

@ -1,5 +0,0 @@
<?php
namespace Ulmus\Annotation\Property\Relation;
class Ignore implements \Notes\Annotation {}

View File

@ -1,19 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class Virtual extends Field {
public bool $readonly = true;
public \Closure $closure;
public string $method;
public function __construct(? \Closure $closure = null)
{
if ( $closure !== null ) {
$this->closure = $closure;
}
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
use Ulmus\Query;
class Where implements \Notes\Annotation {
public /* stringable */ $field;
public $value;
public string $operator;
public string $condition;
public function __construct(/* ? Stringable */ $field = null, $value = null, ? string $operator = null, ? string $condition = null)
{
if ( $field !== null ) {
$this->field = $field;
}
if ( $value !== null ) {
$this->value = $value;
}
$this->operator = $operator !== null ? $operator : Query\Where::OPERATOR_EQUAL;
$this->condition = $condition !== null ? $condition : Query\Where::CONDITION_AND;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ulmus\Annotation\Property;
class WithJoin implements \Notes\Annotation {
protected array $joins;
public function __construct(/*Stringable|array|null*/ $joins = null)
{
if ( $joins ) {
$this->joins = (array)$joins;
}
}
}

View File

@ -10,6 +10,7 @@ class Table implements AdapterAttributeInterface {
public ? string $schema = null,
public ? string $adapter = null,
public ? string $engine = null,
public string $description = "",
) {}
public function adapter() : false|string

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Binary extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "binary",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Char extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "char",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Double extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "float",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Json extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "json",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -5,8 +5,12 @@ namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Time extends \Ulmus\Attribute\Property\Field
{
public function __construct(? string $type = "time", ? int $length = null)
{
parent::__construct($type, $length);
}
public function __construct(
public ? string $name = null,
public ? string $type = "time",
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
) {}
}

View File

@ -5,8 +5,12 @@ namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Timestamp extends \Ulmus\Attribute\Property\Field
{
public function __construct(? string $type = "timestamp", ? int $length = null)
{
parent::__construct($type, $length);
}
public function __construct(
public ? string $name = null,
public ? string $type = "timestamp",
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Varbinary extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "varbinary",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Varchar extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "varchar",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Year extends \Ulmus\Attribute\Property\Field
{
public function __construct(
public ? string $name = null,
public ? string $type = "year",
public null|int|string $length = null,
public ? int $precision = null,
public array $attributes = [],
public bool $nullable = false,
public mixed $default = null,
public bool $readonly = false,
public string $description = "",
) {}
}

View File

@ -5,7 +5,7 @@ namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute;
#[\Attribute]
class Join {
class Join implements ResettablePropertyInterface {
public function __construct(
public string $type,
public null|string|\Stringable|array $key = null,

View File

@ -5,7 +5,7 @@ namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Relation {
class Relation implements ResettablePropertyInterface {
public function __construct(
public Relation\RelationTypeEnum|string $type,
public \Stringable|string|array $key = "",
@ -35,7 +35,7 @@ class Relation {
try {
$e = $this->entity;
} catch (\Throwable $ex) {
throw new \Exception("Your @Relation annotation seems to be missing an `entity` entry.");
throw new \Exception("Your @Relation attribute seems to be missing an `entity` entry.");
}
return new $e();

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\Attribute\Property;
interface ResettablePropertyInterface
{
}

View File

@ -3,7 +3,7 @@
namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Virtual extends Field {
class Virtual extends Field implements ResettablePropertyInterface {
public bool $readonly = true;

View File

@ -2,12 +2,10 @@
namespace Ulmus\Common;
use Ulmus\Annotation\Annotation;
use Ulmus\Attribute;
use Ulmus\Attribute\Property\{ Field };
use Ulmus\Migration\FieldDefinition;
use Ulmus\Ulmus,
Ulmus\Adapter\AdapterInterface,
Ulmus\Annotation\Property\Field,
Ulmus\Query\WhereRawParameter;
class EntityField implements WhereRawParameter
@ -30,7 +28,7 @@ class EntityField implements WhereRawParameter
public function name($useAlias = true) : string
{
$name = $this->entityResolver->searchFieldAnnotation($this->name, [ Attribute\Property\Field::class, Field::class ] )->name ?? $this->name;
$name = $this->entityResolver->searchFieldAnnotation($this->name, [ Field::class ] )->name ?? $this->name;
$name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD);

View File

@ -2,15 +2,17 @@
namespace Ulmus\Common;
use Notes\Common\ReflectedClass;
use Notes\Common\ReflectedProperty;
use Psr\SimpleCache\CacheInterface;
use Ulmus\Ulmus,
Ulmus\Annotation\Classes\Table,
Ulmus\Annotation\Property\Field,
Ulmus\Annotation\Property\Virtual,
Ulmus\Annotation\Property\Relation,
Ulmus\Attribute;
Ulmus\Attribute\Obj\Table,
Ulmus\Attribute\Obj\AdapterAttributeInterface,
Ulmus\Attribute\Property\Field,
Ulmus\Attribute\Property\Relation,
Ulmus\Attribute\Property\Virtual;
use Notes\Annotation;
use Notes\Common\ReflectedAttribute;
use Notes\ObjectReflection;
@ -24,13 +26,7 @@ class EntityResolver {
public string $entityClass;
public array $uses;
public array $class;
public array $properties;
public array $methods;
public ReflectedClass $reflectedClass;
protected array $fieldList = [];
@ -38,14 +34,10 @@ class EntityResolver {
{
$this->entityClass = $entityClass;
list($this->uses, $this->class, $this->methods, $this->properties) = array_values(
ObjectReflection::fromClass($entityClass, $cache)->read()
);
$this->resolveAnnotations();
$this->reflectedClass = ObjectReflection::fromClass($entityClass, $cache)->reflectClass();
}
public function field($name, $fieldKey = self::KEY_ENTITY_NAME, $throwException = true) : ? array
public function field($name, $fieldKey = self::KEY_ENTITY_NAME, $throwException = true) : null|ReflectedProperty
{
try{
return $this->fieldList($fieldKey)[$name] ?? null;
@ -59,7 +51,7 @@ class EntityResolver {
return null;
}
public function searchField($name) : null|array
public function searchField($name) : null|ReflectedProperty
{
return $this->field($name, self::KEY_ENTITY_NAME, false) ?: $this->field($name, self::KEY_COLUMN_NAME, false);
}
@ -68,25 +60,25 @@ class EntityResolver {
{
$fieldList = [];
foreach($this->properties as $item) {
foreach($item['tags'] ?? [] as $tag) {
if ( $tag['object'] instanceof Field or $tag['object'] instanceof Attribute\Property\Field ) {
if ( $skipVirtual && ($tag['object'] instanceof Virtual or $tag['object'] instanceof Attribute\Property\Virtual )) {
foreach($this->reflectedClass->getProperties(true) as $item) {
foreach($item->getAttributes() as $tag) {
if ( $tag->object instanceof Field ) {
if ( $skipVirtual && $tag->object instanceof Virtual ) {
break;
}
switch($fieldKey) {
case static::KEY_LC_ENTITY_NAME:
$key = strtolower($item['name']);
$key = strtolower($item->name);
break;
case static::KEY_ENTITY_NAME:
$key = $item['name'];
$key = $item->name;
break;
case static::KEY_COLUMN_NAME:
$key = strtolower($tag['object']->name ?? $item['name']);
$key = strtolower($tag->object->name ?? $item->name);
break;
default:
@ -103,26 +95,24 @@ class EntityResolver {
return $fieldList;
}
public function relation(string $name) : ? array
public function relation(string $name) : array
{
$property = $this->reflectedClass->getProperties(true)[$name] ?? false;
try{
if ( null !== ( $this->properties[$name] ?? null ) ) {
foreach($this->properties[$name]['tags'] ?? [] as $tag) {
if ( $tag['object'] instanceof Relation or $tag['object'] instanceof Attribute\Property\Relation ) {
return $this->properties[$name];
if ( $property ) {
foreach($property->getAttributes() as $tag) {
if ( $tag->object instanceof Relation ) {
return $property;
}
}
}
return [];
}
catch(\Throwable $e) {
# if ( $throwException) {
throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}");
# }
throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}");
}
return null;
}
public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object
@ -130,25 +120,19 @@ class EntityResolver {
return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null;
}
public function searchFieldAnnotationList(string $field, array|object|string $annotationType, bool $caseSensitive = true) : array
public function searchFieldAnnotationList(string $field, array|object|string $attributeType, bool $caseSensitive = true) : false|array
{
$list = [];
$search = $caseSensitive ? $this->properties : array_change_key_case($this->properties, \CASE_LOWER);
$properties = $this->reflectedClass->getProperties(true);
$annotations = is_array($annotationType) ? $annotationType : [ $annotationType ];
$search = $caseSensitive ? $properties : array_change_key_case($properties, \CASE_LOWER);
if ( null !== ( $search[$field] ?? null ) ) {
foreach($search[$field]['tags'] ?? [] as $tag) {
foreach($annotations as $annotation) {
if ( $tag['object'] instanceof $annotation ) {
$list[] = $tag['object'];
}
}
}
return array_map(fn(ReflectedAttribute $e) => $e->object, $search[$field]->getAttributes((array) $attributeType));
}
return $list;
return false;
}
public function tableName($required = false) : string
@ -164,7 +148,7 @@ class EntityResolver {
return $table->name ?? "";
}
public function tableAnnotation($required = false) : null|Table|Attribute\Obj\Table
public function tableAnnotation($required = false) : null|Table
{
if ( null === $table = $this->getTableAttribute() ) {
if ($required) {
@ -175,7 +159,7 @@ class EntityResolver {
return $table;
}
public function databaseName() : ? string
public function databaseName() : null|string
{
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null;
}
@ -204,7 +188,7 @@ class EntityResolver {
return $this->sqlAdapter();
}
public function schemaName(bool $required = false) : ? string
public function schemaName(bool $required = false) : null|string
{
if ( null === $table = $this->getTableAttribute() ) {
throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation");
@ -217,10 +201,10 @@ class EntityResolver {
return $table->schema ?? null;
}
public function getPrimaryKeyField() : ? array
public function getPrimaryKeyField() : null|array
{
foreach($this->fieldList() as $key => $value) {
$field = $this->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]);
$field = $this->searchFieldAnnotation($key, [ Field::class ]);
if ( null !== $field ) {
if ( false !== ( $field->attributes['primary_key'] ?? false ) ) {
return [ $key => $field ];
@ -243,121 +227,22 @@ class EntityResolver {
protected function getAdapterInterfaceAttribute() : null|object
{
return $this->getAttributeImplementing(Attribute\Obj\AdapterAttributeInterface::class);
return $this->getAttributeImplementing(AdapterAttributeInterface::class);
}
protected function getTableAttribute()
{
return $this->getAttributeImplementing(Attribute\Obj\Table::class) ?: $this->getAnnotationFromClassname( Table::class, false );
return $this->getAttributeImplementing(Table::class);
}
/**
* Transform an annotation into it's object's counterpart
*/
public function getAnnotationFromClassname(string $className, bool $throwError = true) : ? object
public function getAttributeImplementing(string $interface) : null|object
{
if ( $name = $this->uses[$className] ?? false ) {
foreach(array_reverse($this->class['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
foreach($this->properties as $item) {
foreach(array_reverse($item['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
}
}
foreach($this->methods as $item) {
foreach(array_reverse($item['tags']) as $item) {
if ( $item['tag'] === $name ) {
return $this->instanciateAnnotationObject($item);
}
}
}
}
if ($throwError) {
throw new \TypeError("Annotation `$className` could not be found within your object `{$this->entityClass}`");
}
}
elseif ($throwError) {
throw new \InvalidArgumentException("Class `$className` was not found within {$this->entityClass} uses statement (or it's children / traits)");
}
return null;
}
public function getAttributeImplementing(string $interface) : ? object
{
foreach (array_reverse($this->class['tags']) as $item) {
if ($item['object'] instanceof $interface) {
return $item['object'];
foreach ($this->reflectedClass->getAttributes(true) as $item) {
if ($item->object instanceof $interface) {
return $item->object;
}
}
return null;
}
public function instanciateAnnotationObject(array|\ReflectionAttribute $tagDefinition) : object
{
if ($tagDefinition instanceof \ReflectionAttribute) {
$obj = $tagDefinition->newInstance();
}
else {
$arguments = $this->extractArguments($tagDefinition['arguments']);
if (false === $class = array_search($tagDefinition['tag'], $this->uses)) {
throw new \InvalidArgumentException("Annotation class `{$tagDefinition['tag']}` was not found within {$this->entityClass} uses statement (or it's children / traits)");
}
$obj = new $class(... $arguments['constructor']);
foreach ($arguments['setter'] as $key => $value) {
$obj->$key = $value;
}
}
return $obj;
}
/**
* Extracts arguments from an Annotation definition, easing object's declaration.
*/
protected function extractArguments(array $arguments) : array
{
$list = [
'setter' => [],
'constructor' => [],
];
ksort($arguments);
foreach($arguments as $key => $value) {
$list[ is_int($key) ? 'constructor' : 'setter' ][$key] = $value;
}
return $list;
}
protected function resolveAnnotations()
{
foreach($this->class['tags'] as &$tag) {
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
foreach($this->properties as &$property) {
foreach($property['tags'] as &$tag){
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
}
foreach($this->methods as &$method) {
foreach($method['tags'] as &$tag){
$tag['object'] ??= $this->instanciateAnnotationObject($tag);
}
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Ulmus\Common\PdoObject;
class SqlPdoObject extends \Ulmus\Common\PdoObject
{
protected int $openedTransaction = 0;
public function beginTransaction(): bool
{
if ( 0 === $this->openedTransaction++ ) {
return $this->openTransaction();
}
return $this->exec('SAVEPOINT transaction_'.$this->openedTransaction) !== false;
}
public function commit() : bool
{
if ( 0 === --$this->openedTransaction) {
return parent::commit();
}
return false;
}
public function rollback() : bool
{
if (0 !== $this->openedTransaction) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
}
return parent::rollback();
}
protected function openTransaction() : bool
{
return parent::beginTransaction();
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Ulmus\Common\PdoObject;
class SqlitePdoObject extends SqlPdoObject
{
public bool $inTransaction = false;
public function inTransaction(): bool
{
return $this->openedTransaction > 0;
}
public function commit(): bool
{
if ( 0 === --$this->openedTransaction) {
return $this->exec("COMMIT") !== false;
}
return false;
}
protected function openTransaction(): bool
{
return $this->exec("BEGIN IMMEDIATE TRANSACTION") !== false;
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace Ulmus\Entity;
use Generator;
use Ulmus\Attribute\Property\Field;
use Ulmus\Attribute\Property\Relation;
use Ulmus\Common\EntityField;
use Ulmus\Common\EntityResolver;
use Ulmus\Ulmus;
class DatasetHandler
{
public function __construct(
protected EntityResolver $entityResolver,
protected bool $entityStrictFieldsDeclaration = false,
) {}
public function pull(object $entity) : Generator
{
foreach($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) {
$attribute = $this->entityResolver->searchFieldAnnotation($key,[ Field::class ]);
if ( $entity->__isset($key) ) {
yield $attribute->name ?? $key => $entity->$key;
}
elseif ( $field->allowsNull() ) {
yield $attribute->name ?? $key => null;
}
}
}
public function push(iterable $dataset) : Generator|array
{
$unmatched = [];
foreach($dataset as $key => $value) {
$field = $this->entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? $this->entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
if ( $field === null ) {
if ($this->entityStrictFieldsDeclaration ) {
throw new \Exception("Field `$key` can not be found within your entity ".static::class);
}
else {
$unmatched[$key] = $value;
}
continue;
}
$type = $field->getTypes()[0];
if ( is_null($value) ) {
yield $field->name => null;
}
elseif ( $field->expectType('array') ) {
if ( is_string($value)) {
if (substr($value, 0, 1) === "a") {
yield $field->name => unserialize($value);
}
else {
$data = json_decode($value, true);
if (json_last_error() !== \JSON_ERROR_NONE) {
throw new \Exception(sprintf("JSON error while decoding in EntityTrait : '%s' given %s", json_last_error_msg(), $value));
}
yield $field->name => $data;
}
}
elseif ( is_array($value) ) {
yield $field->name => $value;
}
}
elseif ( EntityField::isScalarType($type->type) ) {
if ( $type->type === 'string' ) {
$attribute = $this->entityResolver->searchFieldAnnotation($field->name, [ Field::class ] );
if ( $attribute->length ?? null ) {
$value = mb_substr($value, 0, $attribute->length);
}
}
elseif ( $type->type === 'bool' ) {
$value = (bool) $value;
}
yield $field->name => $value;
}
elseif ( $value instanceof \UnitEnum ) {
yield $field->name => $value;
}
elseif (enum_exists($type->type)) {
yield $field->name => $type->type::from($value);
}
elseif ( ! $type->builtIn ) {
try {
yield $field->name => Ulmus::instanciateObject($type->type, [ $value ]);
}
catch(\Error $e) {
throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field->name));
}
}
}
return $unmatched;
}
public function pullRelation(object $entity) : Generator
{
foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field){
$relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] );
if ($relation) {
$ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) {
if ( $relation->isOneToOne() ) {
# @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE !
yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor();
}
else {
# empty collection
yield $name => [];
}
continue;
}
# @TODO Must fix recursive bug.. this last check is way too basic to work
if ( $entity->__isset($name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $entity->__isset($name) ?? null ) {
if ( is_iterable($value) ) {
$list = [];
foreach($value as $entityObj) {
$list[] = $entityObj->entityGetDataset(false);
}
yield $name => $list;
}
elseif ( is_object($value) ) {
yield $name => $value->entityGetDataset(false);
}
}
}
}
}
}
}

View File

@ -12,12 +12,20 @@ class Datetime extends \DateTime implements EntityObjectInterface {
{
$value = $arguments[0];
# From Timestamp
if ( is_numeric($value) ) {
return new static("@$value");
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 new static($value);
return $obj;
}
public function save()

View File

@ -2,6 +2,7 @@
namespace Ulmus\Entity\InformationSchema;
use Notes\Common\ReflectedProperty;
use Ulmus\Entity\Field\Datetime;
use Ulmus\{Attribute\Obj\Table};
@ -12,6 +13,9 @@ class Column
{
use \Ulmus\EntityTrait;
#[Field\Id]
public ? id $srs_id;
#[Field(name: "TABLE_CATALOG", length: 512)]
public string $tableCatalog;
@ -78,4 +82,44 @@ class Column
#[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
public ? string $generationExpression;
public function matchFieldDefinition(ReflectedProperty $definition) : bool
{
$nullable = $this->nullable === 'YES';
if ($nullable !== $definition->allowsNull()) {
return false;
}
if ( isset($definition->value) && $this->canHaveDefaultValue() ) {
if ( $definition->value !== $this->defaultValue()) {
return false;
}
}
elseif (! isset($definition->value)) {
if ( ! $this->defaultValueIsNull() ) {
return false;
}
}
return true;
}
protected function defaultValueIsNull() : bool
{
return $this->defaultValue() === null;
}
protected function defaultValue() : mixed
{
if (is_numeric($this->default)) {
return (int) $this->default;
}
return $this->default;
}
protected function canHaveDefaultValue() : bool
{
return true;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Ulmus\Entity\Mysql;
class Column extends \Ulmus\Entity\InformationSchema\Column
{
# TODO ! Handle FUNCTIONAL default value
protected function canHaveDefaultValue(): bool
{
return ! in_array(strtoupper($this->dataType), [
'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'JSON', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'GEOMETRY'
]);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Ulmus\Entity\Mysql;
use Ulmus\EntityCollection,
Ulmus\Entity\Field\Datetime;
use Ulmus\{Attribute\Obj\Table as TableObj};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
#[TableObj(name: "tables", database: "information_schema")]
class Table extends \Ulmus\Entity\InformationSchema\Table
{
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
#[Where('TABLE_SCHEMA', fieldValue: [ Column::class, 'tableSchema' ])]
public EntityCollection $columns;
}

View File

@ -2,6 +2,7 @@
namespace Ulmus\Entity\Sqlite;
use Notes\Common\ReflectedProperty;
use Ulmus\EntityCollection;
use Ulmus\{Attribute\Obj\Table};
@ -32,4 +33,29 @@ class Column
#[Field(name: "pk")]
public bool $primaryKey;
public function matchFieldDefinition(ReflectedProperty $definition) : bool
{
if ($this->notNull === $definition->allowsNull()) {
return false;
}
if (isset($definition->value)) {
if ( $definition->value !== $this->defaultValue) {
return false;
}
}
elseif (! isset($definition->value)) {
if ( ! $this->defaultValueIsNull() ) {
return false;
}
}
return true;
}
protected function defaultValueIsNull() : bool
{
return $this->defaultValue === null || $this->defaultValue === "NULL";
}
}

View File

@ -57,7 +57,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return $this->filtersCollection($callback, true, false)->toArray();
}
public function filtersCount(Callable $callback) : array
public function filtersCount(Callable $callback) : int
{
return $this->filtersCollection($callback, true, false)->count();
}

View File

@ -4,7 +4,15 @@ namespace Ulmus;
use Notes\Attribute\Ignore;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\{Common\EntityResolver, Common\EntityField, Entity\EntityInterface, QueryBuilder\QueryBuilderInterface};
use Ulmus\{Attribute\Property\Join,
Attribute\Property\Relation,
Attribute\Property\ResettablePropertyInterface,
Attribute\Property\Virtual,
Common\EntityResolver,
Common\EntityField,
Entity\DatasetHandler,
Entity\EntityInterface,
QueryBuilder\QueryBuilderInterface};
use Ulmus\SearchRequest\{Attribute\SearchParameter,
SearchMethodEnum,
SearchRequestInterface,
@ -14,6 +22,9 @@ use Ulmus\SearchRequest\{Attribute\SearchParameter,
trait EntityTrait {
use EventTrait;
#[Ignore]
public array $entityLoadedDataset = [];
#[Ignore]
protected bool $entityStrictFieldsDeclaration = false;
@ -21,14 +32,23 @@ trait EntityTrait {
protected array $entityDatasetUnmatchedFields = [];
#[Ignore]
public array $entityLoadedDataset = [];
protected DatasetHandler $datasetHandler;
#[Ignore]
public function __construct(array $dataset = null) {
public function __construct(iterable|null $dataset = null)
{
$this->initializeEntity($dataset);
}
#[Ignore]
public function initializeEntity(iterable|null $dataset = null) : void
{
if ($dataset) {
$this->entityFillFromDataset($dataset);
$this->fromArray($dataset);
}
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
$this->resetVirtualProperties();
}
@ -37,96 +57,55 @@ trait EntityTrait {
{
$loaded = $this->isLoaded();
$entityResolver = $this->resolveEntity();
$handler = $this->datasetHandler->push($dataset);
foreach($dataset as $key => $value) {
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null;
$field ??= $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
foreach($handler as $field => $value) {
$this->$field = $value;
}
if ( $field === null ) {
if ($this->entityStrictFieldsDeclaration ) {
throw new \Exception("Field `$key` can not be found within your entity ".static::class);
}
else {
$this->entityDatasetUnmatchedFields[$key] = $value;
}
}
elseif ( is_null($value) ) {
$this->{$field['name']} = null;
}
elseif ( $field['type'] === 'array' ) {
if ( is_string($value)) {
if (substr($value, 0, 1) === "a") {
$this->{$field['name']} = unserialize($value);
}
else {
$data = json_decode($value, true);
$this->entityDatasetUnmatchedFields = $handler->getReturn();
if (json_last_error() !== \JSON_ERROR_NONE) {
throw new \Exception(sprintf("JSON error while decoding in EntityTrait : '%s' given %s", json_last_error_msg(), $value));
}
$this->{$field['name']} = $data;
}
}
elseif ( is_array($value) ) {
$this->{$field['name']} = $value;
}
}
elseif ( EntityField::isScalarType($field['type']) ) {
if ( $field['type'] === 'string' ) {
$annotation = $entityResolver->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, ] );
if ( $annotation->length ?? null ) {
$value = mb_substr($value, 0, $annotation->length);
}
}
elseif ( $field['type'] === 'bool' ) {
$value = (bool) $value;
}
$this->{$field['name']} = $value;
}
elseif ( $value instanceof \UnitEnum ) {
$this->{$field['name']} = $value;
}
elseif (enum_exists($field['type'])) {
$this->{$field['name']} = $field['type']::from($value);
}
elseif ( ! $field['builtin'] ) {
try {
$this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]);
}
catch(\Error $e) {
$f = $field['type'];
throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field['name']));
}
}
# Keeping original data to diff on UPDATE query
if ( ! $loaded /* || $isLoadedDataset */ ) {
#if ( $field !== null ) {
# $annotation = $entityResolver->searchFieldAnnotation($field['name'], new Field() );
# $this->entityLoadedDataset[$annotation ? $annotation->name : $field['name']] = $dataset; # <--------- THIS TO FIX !!!!!!
#}
$this->entityLoadedDataset = array_change_key_case($dataset, \CASE_LOWER);
}
elseif ($overwriteDataset) {
$this->entityLoadedDataset = iterator_to_array(array_change_key_case($dataset, \CASE_LOWER)) + $this->entityLoadedDataset;
}
# 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;
}
$dataset = [];
foreach($this->datasetHandler->pull($this) as $field => $value) {
$dataset[$field] = $rewriteValue ? static::repository()->adapter->adapter()->writableValue($value) : $value;
}
if ($includeRelations) {
foreach($this->datasetHandler->pullRelation($this) as $field => $object) {
$dataset[$field] = $object;
}
}
return $dataset;
}
#[Ignore]
public function resetVirtualProperties() : self
{
foreach($this->resolveEntity()->properties as $prop => $property) {
foreach($property['tags'] as $tag) {
if ( in_array(strtolower($tag['tag']), [ 'relation', 'join', 'virtual' ] ) ) {
unset($this->$prop);
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) {
if ( $tag->object instanceof ResettablePropertyInterface ) {
unset($this->$field);
}
}
}
@ -144,74 +123,6 @@ trait EntityTrait {
return $this->entityFillFromDataset($dataset);
}
public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false, bool $rewriteValue = true) : array
{
if ( $returnSource ) {
return $this->entityLoadedDataset;
}
$dataset = [];
$entityResolver = $this->resolveEntity();
foreach($entityResolver->fieldList(Common\EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) {
$annotation = $entityResolver->searchFieldAnnotation($key, [ Attribute\Property\Field::class, Field::class ]);
if ( isset($this->$key) ) {
$dataset[$annotation->name ?? $key] = $rewriteValue ?
static::repository()->adapter->adapter()->writableValue($this->$key)
:
$this->$key;
}
elseif ( $field['nullable'] ) {
$dataset[$annotation->name ?? $key] = null;
}
}
if ($includeRelations) {
foreach($entityResolver->properties as $name => $field){
$relation = $entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ] );
if ($relation) {
$ignore = $entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) {
if ( $relation->isOneToOne() ) {
# empty object
$dataset[$name] = ( new \ReflectionClass($field['type']) )->newInstanceWithoutConstructor();
}
else {
# empty collection
$dataset[$name] = [];
}
continue;
}
# @TODO Must fix recursive bug.. this last check is way too basic to work
if ( isset($this->$name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $this->$name ?? null ) {
if ( is_iterable($value) ) {
$list = [];
foreach($value as $entity) {
$list[] = $entity->entityGetDataset(false);
}
$dataset[$name] = $list;
}
elseif ( is_object($value) ) {
$dataset[$name] = $value->entityGetDataset(false);
}
}
}
}
}
}
return $dataset;
}
#[Ignore]
public function toArray($includeRelations = false, array $filterFields = null, bool $rewriteValue = true) : array
{
@ -326,7 +237,9 @@ trait EntityTrait {
{
$default = ( $alias === false ? '' : Repository::DEFAULT_ALIAS ); # bw compatibility, to be deprecated
return new EntityField(static::class, $name, $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default, Ulmus::resolveEntity(static::class));
$alias = $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default;
return new EntityField(static::class, $name, $alias, Ulmus::resolveEntity(static::class));
}
#[Ignore]
@ -338,7 +251,7 @@ trait EntityTrait {
}
#[Ignore]
public static function searchRequest(...$arguments) : SearchRequestInterface
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:

View File

@ -2,6 +2,7 @@
namespace Ulmus\Migration;
use Notes\Common\ReflectedProperty;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation\Property\Field;
use Ulmus\Attribute;
@ -29,25 +30,28 @@ class FieldDefinition {
public AdapterInterface $adapter;
public function __construct(AdapterInterface $adapter, array $data)
public function __construct(AdapterInterface $adapter, ReflectedProperty $data)
{
$this->adapter = $adapter;
$this->name = $data['name'];
$this->builtIn = $data['builtin'];
$this->tags = $data['tags'];
# Patch coming soon
$type = $data->getTypes()[0];
if (isset($data['value'])) {
$this->default = $data['value'];
$this->name = $data->name;
$this->builtIn = $type->builtIn;
$this->tags = $data->getAttributes();
if (isset($data->value)) {
$this->default = $data->value;
}
$field = $this->getFieldTag();
$adapter->whitelistAttributes($field->attributes);
$this->type = $field->type ?? $data['type'];
$this->type = $field->type ?? $type->type;
$this->length = $field->length ?? null;
$this->precision = $field->precision ?? null;
$this->nullable = $data['nullable'] ?: $field->nullable;
$this->nullable = $data->allowsNull() ?: $field->nullable;
$this->update = $field->attributes['update'] ?? null;
}
@ -76,11 +80,11 @@ class FieldDefinition {
]));
}
public function getFieldTag() : Field|Attribute\Property\Field|null
public function getFieldTag() : Attribute\Property\Field|null
{
$field = array_filter($this->tags, fn($item) => $item['object'] instanceof Field || $item['object'] instanceof Attribute\Property\Field);
$field = array_filter($this->tags, fn($item) => $item->object instanceof Attribute\Property\Field);
return array_pop($field)['object'];
return array_pop($field)->object;
}
public function getColumnName() : ? string

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\Migration\Sql;
class SqliteMigration
{
}

View File

@ -3,8 +3,7 @@
namespace Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
use Ulmus\Common\EntityField;
class Alter extends Fragment {

View File

@ -3,8 +3,7 @@
namespace Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
use Ulmus\Common\EntityField;
class Create extends Fragment {

View File

@ -237,6 +237,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
if ($value === 0) {
$this->removeFragment(Query\Limit::class);
$this->removeFragment(Query\Offset::class);
}
return $this;

View File

@ -2,16 +2,9 @@
namespace Ulmus;
use Ulmus\Annotation\Property\{Field,
OrderBy,
Where,
Having,
Relation,
Filter,
Join,
FilterJoin,
WithJoin,
Relation\Ignore as RelationIgnore};
use Ulmus\Attribute\Property\{
Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin
};
use Ulmus\Common\EntityResolver;
use Ulmus\Repository\RepositoryInterface;
use Ulmus\Repository\WithOptionEnum;
@ -89,7 +82,16 @@ class Repository implements RepositoryInterface
$this->select( "DISTINCT COUNT(*) OVER ()" );
}
else {
$this->select(Common\Sql::function("COUNT", '*'));
$pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if (count($pk) === 1) {
$field = key($pk);
$this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field))));
}
else {
$this->select(Common\Sql::function('COUNT', Common\Sql::raw('*')));
}
}
$this->selectSqlQuery();
@ -99,6 +101,11 @@ class Repository implements RepositoryInterface
return Ulmus::runSelectQuery($this->queryBuilder, $this->adapter)->fetchColumn(0);
}
public function exists(mixed $primaryKey) : bool
{
return $this->wherePrimaryKey($primaryKey)->count() !== 0;
}
public function deleteOne()
{
return $this->limit(1)->deleteSqlQuery()->runDeleteQuery();
@ -134,8 +141,6 @@ class Repository implements RepositoryInterface
return $this->deleteFromPk($entity->$pkField);
}
return false;
}
public function destroyAll(EntityCollection $collection) : void
@ -365,7 +370,7 @@ class Repository implements RepositoryInterface
$dataset = $this->generateDatasetDiff($entity, $oldValues);
foreach($dataset as $field => $value) {
if ( false === ( $this->entityResolver->searchFieldAnnotation($field, [ Attribute\Property\Field::class, Field::class ], false)->readonly ?? false ) ) {
if ( false === ( $this->entityResolver->searchFieldAnnotation($field, [ Field::class, Field::class ], false)->readonly ?? false ) ) {
$intersect[$field] = $field;
}
}
@ -398,8 +403,8 @@ class Repository implements RepositoryInterface
$prependField and ($prependField .= "$");
foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) {
$this->select(sprintf("%s.$key as {$prependField}{$field['name']}", $this->escapeIdentifier($alias)));
if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) {
$this->select(sprintf("%s.$key as {$prependField}{$field->name}", $this->escapeIdentifier($alias)));
}
}
@ -411,9 +416,9 @@ class Repository implements RepositoryInterface
$fieldlist = [];
foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) {
if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) {
$fieldlist[] = $key;
$fieldlist[] = $entity::field($field['name'], $this->escapeIdentifier($alias));
$fieldlist[] = $entity::field($field->name, $this->escapeIdentifier($alias));
}
}
@ -626,26 +631,26 @@ class Repository implements RepositoryInterface
$this->joined[$item] = true;
}
$annotation = $this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Join::class, Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Relation::class, Relation::class ]);
$attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]);
$isRelation = ( $annotation instanceof Relation ) || ($annotation instanceof Attribute\Property\Relation);
$isRelation = ( $attribute instanceof Relation ) || ($attribute instanceof Relation);
if ($isRelation && ( $annotation->isManyToMany() )) {
throw new Exception("Many-to-many relation can not be preloaded within joins.");
if ($isRelation && ( $attribute->isManyToMany() )) {
throw new \Exception("Many-to-many relation can not be preloaded within joins.");
}
if ( $annotation ) {
$alias = $annotation->alias ?? $item;
if ( $attribute ) {
$alias = $attribute->alias ?? $item;
$entity = $annotation->entity ?? $this->entityResolver->properties[$item]['type'];
$entity = $attribute->entity ?? $this->entityResolver->reflectedClass->getProperties(true)[$item]->getTypes()[0]->type;
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ]) ) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
$escAlias = $this->escapeIdentifier($alias);
$fieldName = $this->escapeIdentifier($key);
$name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], [ Attribute\Property\Field::class, Field::class ])->name ?? $field['name'];
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
$this->select("$escAlias.$fieldName as $alias\${$name}");
@ -655,7 +660,7 @@ class Repository implements RepositoryInterface
$this->open();
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Where::class, Where::class ] ) as $condition) {
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);
}
@ -663,26 +668,26 @@ class Repository implements RepositoryInterface
}
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [Attribute\Property\Having::class, Having::class]) as $condition) {
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, [Attribute\Property\Filter::class, Filter::class]) as $filter) {
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($annotation->key) ? $this->entityClass::field($annotation->key) : $annotation->key;
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
$foreignKey = is_string($annotation->foreignKey) ? $entity::field($annotation->foreignKey, $alias) : $annotation->foreignKey;
$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, [ Attribute\Property\Where::class, Where::class ]) as $condition) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
if ( ! is_object($condition->field) ) {
$field = $this->entityClass::field($condition->field);
}
@ -700,14 +705,14 @@ class Repository implements RepositoryInterface
}
if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [Attribute\Property\FilterJoin::class, FilterJoin::class]) as $filter) {
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 annotation.");
throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute.");
}
}
@ -727,7 +732,7 @@ class Repository implements RepositoryInterface
# Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) {
if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Attribute\Property\Relation::class, Relation::class ]) ) {
if ( $relation = $this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]) ) {
$alias = $relation->alias ?? $item;
if ( $relation->isManyToMany() ) {
@ -748,11 +753,11 @@ class Repository implements RepositoryInterface
# $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true);
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Where::class, Where::class ]) as $condition) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
$repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
}
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Attribute\Property\Having::class, Having::class ] ) as $condition) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ] ) as $condition) {
$repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
}
@ -769,7 +774,7 @@ class Repository implements RepositoryInterface
$this->select("(" . $r = Common\Sql::raw($repository->queryBuilder->render() . ") as $item\$collection"));
}
else {
throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join annotation.");
throw new \Exception("You referenced field `$item` which do not exist or do not contain a valid @Join attribute.");
}
}
@ -779,9 +784,9 @@ class Repository implements RepositoryInterface
public function loadCollectionRelation(EntityCollection $collection, array|string $fields) : void
{
foreach ((array)$fields as $name) {
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ))) {
$order = $this->entityResolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ]);
$where = $this->entityResolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ]);
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ))) {
$order = $this->entityResolver->searchFieldAnnotationList($name, [ OrderBy::class ]);
$where = $this->entityResolver->searchFieldAnnotationList($name, [ Where::class ]);
$baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type'];
$baseEntityResolver = $baseEntity::resolveEntity();
@ -792,7 +797,7 @@ class Repository implements RepositoryInterface
$repository = $baseEntity::repository();
foreach ($baseEntityResolver->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if (null === $baseEntityResolver->searchFieldAnnotation($field['name'], [ Attribute\Property\Relation\Ignore::class, RelationIgnore::class ])) {
if (null === $baseEntityResolver->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) {
$repository->select($baseEntityResolver->entityClass::field($key));
}
}
@ -956,7 +961,7 @@ class Repository implements RepositoryInterface
{
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$fields = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $fields)));
$this->select($this->entityClass::fields(array_map(fn($f) => $f->object->name ?? $f->name, $fields)));
}
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {

View File

@ -2,20 +2,13 @@
namespace Ulmus\Repository;
use Ulmus\{Attribute\Property\Field,
Ulmus,
Annotation,
Attribute,
Query,
Common,
Common\EntityResolver,
Repository,
Event,
EntityCollection};
use Ulmus\{
Ulmus, Query, Common\EntityResolver, Repository, Event,
};
use Ulmus\Annotation\Property\{Filter, OrderBy, Relation, Relation\Ignore as RelationIgnore, Where, WithJoin, };
use Closure;
use Ulmus\Attribute\Property\{
Filter, Join, OrderBy, Relation, Virtual, Where, WithJoin,
};
class RelationBuilder
{
@ -64,7 +57,7 @@ class RelationBuilder
}
else {
if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class , Relation::class ] ) ) {
if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) {
return $this->instanciateEmptyObject($name, $relation);
}
elseif ($virtual = $this->resolveVirtual($name)) {
@ -77,7 +70,7 @@ class RelationBuilder
protected function resolveVirtual(string $name) : mixed
{
if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Virtual::class, Annotation\Property\Virtual::class ]))) {
if (null !== ($virtual = $this->resolver->searchFieldAnnotation($name, Virtual::class))) {
try {
$arguments = [ $this->entity ];
@ -98,11 +91,11 @@ class RelationBuilder
protected function resolveRelation(string $name) : mixed
{
if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Relation::class ] ) ) ) {
$this->orders = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\OrderBy::class, OrderBy::class ] );
$this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Where::class, Where::class ] );
$this->filters = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\Filter::class, Filter::class ] );
$this->joins = $this->resolver->searchFieldAnnotationList($name, [ Attribute\Property\WithJoin::class, WithJoin::class ] );
if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) ) {
$this->orders = $this->resolver->searchFieldAnnotationList($name, [ OrderBy::class ] );
$this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Where::class ] );
$this->filters = $this->resolver->searchFieldAnnotationList($name, [ Filter::class ] );
$this->joins = $this->resolver->searchFieldAnnotationList($name, [ WithJoin::class ] );
switch( true ) {
case $relation->isOneToOne():
@ -116,7 +109,13 @@ class RelationBuilder
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
return call_user_func([ $this->repository, $relation->function() ]) ?? $this->instanciateEmptyEntity($name, $relation);
$value = call_user_func([ $this->repository, $relation->function() ]) ?? null;
if ($value === null && $this->fieldIsNullable($name) ) {
return null;
}
return $value ?? $this->instanciateEmptyEntity($name, $relation);
case $relation->isOneToMany():
$this->oneToMany($name, $relation);
@ -178,22 +177,22 @@ class RelationBuilder
}
}
protected function instanciateEmptyEntity(string $name, Relation|Attribute\Property\Relation $relation) : object
protected function instanciateEmptyEntity(string $name, Relation $relation) : object
{
$class = $relation->entity ?? $this->resolver->properties[$name]['type'];
$class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
return new $class();
}
protected function instanciateEmptyObject(string $name, Relation|Attribute\Property\Relation $relation) : object
protected function instanciateEmptyObject(string $name, Relation $relation) : object
{
switch( true ) {
case $relation->isOneToOne():
return $this->instanciateEmptyEntity($name, $relation);
case $relation->isOneToMany():
return ($relation->entity ?? $this->resolver->properties[$name]['type'])::entityCollection();
return ($relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type)::entityCollection();
case $relation->isManyToMany():
extract($this->relationAnnotations($name, $relation));
@ -204,28 +203,25 @@ class RelationBuilder
throw new \InvalidArgumentException("Unknown or no relation was provided as relation type.");
}
protected function fetchFromDataset($name, ? array $data = null) : object|bool
protected function fetchFromDataset($name, ? array $data = null) : object|bool|null
{
$annotation = $this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Join::class, Annotation\Property\Join::class ]) ?:
$this->resolver->searchFieldAnnotation($name, [ Attribute\Property\Relation::class, Annotation\Property\Relation::class ]);
$attribute = $this->resolver->searchFieldAnnotation($name, [ Join::class ]) ?:
$this->resolver->searchFieldAnnotation($name, [ Relation::class ]);
if ( $annotation ) {
if ( $attribute ) {
$vars = [];
$len = strlen( $name ) + 1;
$isRelation = ( $annotation instanceof Relation ) || ( $annotation instanceof Attribute\Property\Relation );
$isRelation = $attribute instanceof Relation;
if ( $isRelation && $annotation->isManyToMany() ) {
$entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
if ( $isRelation && $attribute->isManyToMany() ) {
$entity = $this->relationAnnotations($name, $attribute)['relationRelation']->entity;
}
else {
$entity = $annotation->entity ?? $this->resolver->properties[$name]['type'];
$entity = $attribute->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
}
$name = strtolower($name);
foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) {
if ( $key === sprintf(static::SUBQUERY_FIELD_SUFFIX, strtolower($name)) ) {
if ($value) {
if ( null === ( $dataset = \json_decode($value, true) ) ) {
@ -251,7 +247,7 @@ class RelationBuilder
return ( new $entity() )->fromArray($data)->resetVirtualProperties();
}
else {
return new $entity();
return $this->fieldIsNullable($name) ? null : new $entity();
}
}
}
@ -259,14 +255,14 @@ class RelationBuilder
return false;
}
public function oneToOne(string $name, Relation|Attribute\Property\Relation $relation) : Repository
public function oneToOne(string $name, Relation $relation) : Repository
{
return $this->oneToMany($name, $relation)->limit(1);
}
public function oneToMany(string $name, Relation|Attribute\Property\Relation $relation) : Repository
public function oneToMany(string $name, Relation $relation) : Repository
{
$baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type'];
$baseEntity = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
$this->repository = $baseEntity::repository();
@ -290,7 +286,7 @@ class RelationBuilder
return $this->applyFilter($this->repository, $name);
}
public function manyToMany(string $name, Relation|Attribute\Property\Relation $relation, Relation|Attribute\Property\Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository
public function manyToMany(string $name, Relation $relation, Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository
{
extract($this->relationAnnotations($name, $relation));
@ -318,7 +314,7 @@ class RelationBuilder
return $this->applyFilter($this->repository, $name);
}
public static function relationAnnotations(string $name, Relation|Attribute\Property\Relation $relation) : array
public static function relationAnnotations(string $name, Relation $relation) : array
{
if ( $relation->isOneToOne() || $relation->isManyToMany() ) {
if ( ! $relation->hasBridge() ) {
@ -326,14 +322,14 @@ class RelationBuilder
}
$bridgeEntity = Ulmus::resolveEntity($relation->bridge);
$bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Attribute\Property\Relation::class, Relation::class ]);
$relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Attribute\Property\Relation::class, Relation::class ]);
$bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, [ Relation::class ]);
$relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, [ Relation::class ]);
if ($relationRelation === null) {
throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
throw new \Exception("@Relation attribute not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
}
$relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type'];
$relationRelation->entity ??= $relation->bridge::resolveEntity()->reflectedClass->getProperties()[$relation->foreignField]->getTypes()[0]->type;
return [
'bridgeEntity' => $bridgeEntity,
@ -344,4 +340,9 @@ class RelationBuilder
return [];
}
protected function fieldIsNullable(string $name) : bool
{
return $this->resolver->reflectedClass->getProperties(true)[$name]->allowsNull();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Ulmus\SearchRequest\Attribute;
enum PropertyValueSource
{
case QueryParams;
case RequestAttribute;
public const ATTRIBUTE_FIRST = [ self::RequestAttribute, self::QueryParams ];
public const QUERY_PARAMS_FIRST = [ self::QueryParams, self::RequestAttribute ];
}

View File

@ -9,5 +9,6 @@ class SearchGroupBy extends SearchParameter
{
public function __construct(
public null|string|\Stringable|array $field = null,
public string $description = "",
) {}
}

View File

@ -8,9 +8,11 @@ use Ulmus\SearchRequest\SearchMethodEnum;
class SearchLike extends SearchParameter
{
public function __construct(
public ? string $parameter = null,
public null|string|array $parameter = null,
public null|string|\Stringable|array $field = null,
public bool $toggle = false,
public SearchMethodEnum $method = SearchMethodEnum::Like,
public string $description = "",
public PropertyValueSource|array $source = PropertyValueSource::QueryParams,
) {}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Ulmus\SearchRequest\Attribute;
use Ulmus\SearchRequest\SearchMethodEnum;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class SearchManual extends SearchParameter
{
public function __construct(
public null|string|array $parameter = null,
public null|string|\Stringable|array $field = null,
public bool $toggle = false,
public SearchMethodEnum $method = SearchMethodEnum::Manual,
public string $description = "",
public PropertyValueSource|array $source = PropertyValueSource::QueryParams,
) {}
}

View File

@ -12,8 +12,9 @@ class SearchOrderBy extends SearchParameter
public const DESCENDING = "DESC";
public function __construct(
public ? string $parameter = null,
public null|string|array $parameter = null,
public null|string|\Stringable|array $field = null,
public null|SearchMethodEnum $order = null,
public string $description = "",
) {}
}

View File

@ -5,8 +5,21 @@ namespace Ulmus\SearchRequest\Attribute;
use Ulmus\SearchRequest\SearchMethodEnum;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class SearchParameter
{
abstract class SearchParameter {
public PropertyValueSource|array $source = PropertyValueSource::QueryParams;
public null|string|\Stringable|array $field = null;
public string $description = "";
public function getSources() : array
{
return is_array($this->source) ? $this->source : [ $this->source ];
}
public function getParameters() : array
{
return array_filter((array) $this->parameter);
}
}

View File

@ -10,5 +10,6 @@ class SearchRequestParameter
public function __construct(
public string $class,
public ? string $alias = null,
public string $description = "",
) {}
}

View File

@ -8,9 +8,11 @@ use Ulmus\SearchRequest\SearchMethodEnum;
class SearchWhere extends SearchParameter
{
public function __construct(
public ? string $parameter = null,
public null|string|array $parameter = null,
public null|string|\Stringable|array $field = null,
public bool $toggle = false,
public SearchMethodEnum $method = SearchMethodEnum::Where,
public string $description = "",
public PropertyValueSource|array $source = PropertyValueSource::QueryParams,
) {}
}

View File

@ -2,9 +2,18 @@
namespace Ulmus\SearchRequest;
use Notes\ObjectReflection;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\SearchRequest\Attribute\{ SearchParameter, SearchWhere, SearchLike, SearchOrderBy, SearchGroupBy, SearchRequestParameter, };
use Ulmus\SearchRequest\Attribute\{PropertyValueSource,
SearchManual,
SearchParameter,
SearchWhere,
SearchLike,
SearchOrderBy,
SearchGroupBy,
SearchRequestParameter};
trait SearchRequestFromRequestTrait
{
@ -18,23 +27,23 @@ trait SearchRequestFromRequestTrait
public function fromRequest(ServerRequestInterface $request)
{
if (method_exists($this, 'prepare')) {
$this->prepare($request);
}
$queryParams = new \ArrayObject(array_filter($request->getQueryParams(), function($i) { return ! is_null($i) && $i !== ""; }));
$this->page = $queryParams->offsetExists('page') ? $queryParams['page'] : 1;
$classReflection = new \ReflectionClass($this);
$classReflection = ObjectReflection::fromClass(static::class)->reflectClass();
foreach($classReflection->getProperties() as $property) {
$attributeList = $property->getAttributes();
foreach($classReflection->getProperties() as $propertyName => $property) {
$attributeList = $property->getAttributes(Attribute\SearchParameter::class);
$attributeReflection = array_filter($attributeList, fn($e) => $e->newInstance() instanceof Attribute\SearchParameter);
if ($attributeList) {
$attribute = $attributeList[0]->object;
if ($attributeReflection) {
$attribute = $attributeReflection[0]->newInstance();
$propertyName = $property->getName();
$fieldName = $attribute->field ?? $propertyName;
$queryParamName = $attribute->parameter ?? $propertyName;
# Field could be defined for another entity class
if (is_array($fieldName)) {
@ -42,7 +51,7 @@ trait SearchRequestFromRequestTrait
}
# Default class using it, if SearchRequestParameter is defined
elseif ($classAttributes = $classReflection->getAttributes(SearchRequestParameter::class)) {
$searchRequestAttribute = $classAttributes[0]->newInstance();
$searchRequestAttribute = $classAttributes[0]->object;
$className = $searchRequestAttribute->class;
$field = $className::field($fieldName, $searchRequestAttribute->alias);
}
@ -51,27 +60,52 @@ trait SearchRequestFromRequestTrait
$field = $fieldName;
}
$value = $queryParams->offsetExists($queryParamName) ? $this->transformValue($attributeList, $queryParams[$queryParamName]) : null;
$value = $this->getValueFromSource($request, $propertyName, $attribute);
if ($attribute instanceof SearchWhere || $attribute instanceof SearchLike) {
if ($attribute->toggle) {
$this->$propertyName = $queryParams->offsetExists($queryParamName);
}
elseif ($value !== null) {
$this->$propertyName = $value;
}
$this->parseAttributeMethod($attribute, $field, $propertyName);
if ($value !== null) {
$value = $this->transformValue($property->getAttributes(Attribute\PropertyValueModifier::class), $value);
}
elseif ($attribute instanceof SearchOrderBy) {
if ($value !== null) {
$this->$propertyName = $value;
}
$this->parseAttributeOrderBy($attribute, $field, $propertyName);
}
elseif ($attribute instanceof SearchGroupBy) {
$this->parseAttributeGroupBy($attribute, $field, $propertyName);
switch(true) {
case $attribute instanceof SearchGroupBy:
$this->parseAttributeGroupBy($attribute, $field, $propertyName);
break;
case $attribute instanceof SearchWhere:
case $attribute instanceof SearchLike:
case $attribute instanceof SearchManual:
if ($attribute->toggle) {
$this->$propertyName = !empty($value);
} elseif ($value !== null) {
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->name, $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) {
$this->$propertyName = $value;
}
}
}
$this->parseAttributeMethod($attribute, $field, $propertyName);
break;
case $attribute instanceof SearchOrderBy:
if ($value !== null) {
$this->$propertyName = $value;
}
$this->parseAttributeOrderBy($attribute, $field, $propertyName);
break;
}
}
}
@ -79,15 +113,43 @@ trait SearchRequestFromRequestTrait
return $this;
}
protected function transformValue(array $attributeList, mixed $value, ) : mixed
protected function getValueFromSource(RequestInterface $request, string $propertyName, SearchParameter $attribute) : mixed
{
$transforms = array_map(function($e) {
$instance = $e->newInstance();
return $instance instanceof Attribute\PropertyValueModifier ? $e->newInstance() : null;
}, $attributeList);
$queryParamName = $attribute->getParameters() ?: [ $propertyName ];
foreach(array_filter($transforms) as $transform) {
$value = $transform->run($value);
foreach($attribute->getSources() as $source) {
switch($source) {
case PropertyValueSource::QueryParams:
$queryParams = new \ArrayObject(array_filter($request->getQueryParams(), function($i) { return ! is_null($i) && $i !== ""; }));
foreach($queryParamName as $param) {
if ($queryParams->offsetExists($param)) {
return $queryParams[$param];
}
}
break;
case PropertyValueSource::RequestAttribute:
foreach($queryParamName as $param) {
$fromAttribute = $request->getAttribute($param, null);
if ($fromAttribute !== null) {
return $fromAttribute;
}
}
break;
}
}
return null;
}
protected function transformValue(array $valueModifierAttributes, mixed $value, ) : mixed
{
foreach($valueModifierAttributes as $transform) {
$value = $transform->object->run($value);
}
return $value;
@ -120,7 +182,9 @@ trait SearchRequestFromRequestTrait
protected function parseAttributeOrderBy(object $attribute, string $field, mixed $propertyName,) : void
{
$this->orders[$field] = $this->$propertyName;
if ( ! empty($this->$propertyName) ) {
$this->orders[$field] = $this->$propertyName;
}
}
protected function parseAttributeGroupBy(object $attribute, string $field, mixed $propertyName,) : void
@ -129,14 +193,4 @@ trait SearchRequestFromRequestTrait
$this->groups[] = $field;
}
}
# @TODO !
/* protected function parseAttributeGroupBy(object $attribute, string $field, string $propertyName,) : void
{
switch ($attribute->order) {
case SearchMethodEnum::GroupBy:
$this->groups[$field];
break;
}
}*/
}

View File

@ -44,7 +44,7 @@ trait SearchRequestPaginationTrait {
public function pageCount() : int
{
return $this->pageCount = ceil($this->count() / $this->limit());
return $this->limit() ? $this->pageCount = ceil($this->count() / $this->limit()) : 1;
}
public function hasPagination() : int