- Added a MssqlRepository

- Adapter some code to allow more flexibility for the new LDAP adapter from ulmus-ldap.
- Insert's fields are now escaped by default.
- A new RelationBuilder was added, a lot of code from the EntityTrait was moved into this. Some code from the Repository class will need to be moved there too.
This commit is contained in:
Dave M. 2021-03-01 16:26:06 +00:00
parent 8f9332fbad
commit 16c6eb57fb
32 changed files with 641 additions and 243 deletions

View File

@ -9,8 +9,9 @@ interface AdapterInterface {
public const IDENTIFIER_TABLE = 2;
public const IDENTIFIER_DATABASE = 3;
public const IDENTIFIER_SCHEMA = 4;
public const IDENTIFIER_VALUE = 5;
public function connect() : PdoObject;
public function connect() : object /* | PdoObject|mixed */;
public function buildDataSourceName() : string;
public function setup(array $configuration) : void;
public function escapeIdentifier(string $segment, int $type) : string;

View File

@ -194,6 +194,9 @@ class MsSQL implements AdapterInterface {
case static::IDENTIFIER_TABLE:
case static::IDENTIFIER_FIELD:
return "[" . str_replace(["[", "]"], [ "[[", "]]" ], $segment) . "]";
case static::IDENTIFIER_VALUE:
return "'$segment'";
}
}

View File

@ -131,6 +131,9 @@ class MySQL implements AdapterInterface {
case static::IDENTIFIER_TABLE:
case static::IDENTIFIER_FIELD:
return "`" . str_replace("`", "``", $segment) . "`";
case static::IDENTIFIER_VALUE:
return "\"$segment\"";
}
}

View File

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,11 @@
<?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

@ -17,7 +17,7 @@ class Relation implements \Ulmus\Annotation\Annotation {
public /*stringable*/ $bridgeKey;
public /*stringable*/ $bridgeForeignKey;
public string $entity;
public string $join;
@ -30,20 +30,20 @@ class Relation implements \Ulmus\Annotation\Annotation {
$this->type = $type;
}
}
public function entity() {
try {
$e = $this->entity;
$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();
}
@ -51,4 +51,24 @@ class Relation implements \Ulmus\Annotation\Annotation {
{
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 hasBridge() : bool
{
return isset($this->bridge);
}
}

View File

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

View File

@ -14,7 +14,7 @@ class Where implements \Ulmus\Annotation\Annotation {
public string $condition;
public function __construct(? string $field = null, $value = null, ? string $operator = null, ? string $condition = null)
public function __construct(/* ? Stringable */ $field = null, $value = null, ? string $operator = null, ? string $condition = null)
{
if ( $field !== null ) {
$this->field = $field;

View File

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

View File

@ -32,7 +32,7 @@ class EntityField
$name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD);
return $useAlias ? "{$this->alias}.$name" : $name;
return $useAlias && $this->alias ? "{$this->alias}.$name" : $name;
}
public static function isScalarType($type) : bool

View File

@ -15,6 +15,8 @@ class EntityResolver {
const KEY_COLUMN_NAME = 02;
const KEY_LC_ENTITY_NAME = 03;
public string $entityClass;
public array $uses;
@ -64,6 +66,11 @@ class EntityResolver {
}
switch($fieldKey) {
case static::KEY_LC_ENTITY_NAME:
$key = strtolower($item['name']);
break;
case static::KEY_ENTITY_NAME:
$key = $item['name'];
break;
@ -158,12 +165,12 @@ class EntityResolver {
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->database;
}
public function databaseAdapter() : \Ulmus\ConnectionAdapter
public function sqlAdapter() : \Ulmus\ConnectionAdapter
{
if ( null !== $table = $this->getAnnotationFromClassname( Table::class ) ) {
if ( $table->adapter ?? null ) {
if ( null === ( $adapter = \Ulmus\Ulmus::$registeredAdapters[$table->adapter] ?? null ) ) {
throw new \Exception("Requested database adapter ( {$table->adapter} ) is not registered.");
throw new \Exception("Requested database adapter `{$table->adapter}` is not registered.");
}
else {
return $adapter;
@ -174,6 +181,14 @@ class EntityResolver {
return Ulmus::$defaultAdapter;
}
/**
* Alias of sqlAdapter
*/
public function databaseAdapter() : \Ulmus\ConnectionAdapter
{
return $this->sqlAdapter();
}
public function schemaName(bool $required = false) : ? string
{
if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) {

View File

@ -65,6 +65,11 @@ class ConnectionAdapter
return $this->pdo ?? $this->pdo = $this->connect()->pdo;
}
public function connector() : object
{
return $this->pdo();
}
/**
* Instanciate an adapter which interact with the data source
* @param string $name An Ulmus adapter or full class name implementing AdapterInterface

View File

@ -233,7 +233,6 @@ class EntityCollection extends \ArrayObject {
public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self
{
foreach($datasets as $dataset) {
$this->append( $this->arrayToEntity($dataset, $entityClass) );
}
@ -247,7 +246,7 @@ class EntityCollection extends \ArrayObject {
throw new \Exception("An entity class name must be provided to be instanciated and populated before insertion into this collection.");
}
$className = $this->entityClass;
$className = $entityClass ?: $this->entityClass;
return ( new $className() )->fromArray($dataset);
}
@ -318,4 +317,9 @@ class EntityCollection extends \ArrayObject {
{
return $this->replaceWith(array_reverse($this->getArrayCopy()));;
}
public function slice(int $offset, ? int $length = null) : self
{
return new self(array_slice($this->getArrayCopy(), $offset, $length));
}
}

View File

@ -8,8 +8,9 @@ use Ulmus\Repository,
Ulmus\Common\EntityField;
use Ulmus\Annotation\Classes\{ Method, Table, Collation, };
use Ulmus\Annotation\Property\{ Field, Filter, Relation, OrderBy, Where, Join, Virtual, On, };
use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, };
use Ulmus\Annotation\Property\{ Field, Filter, Relation, OrderBy, Where, Join, Virtual, On, WithJoin, };
use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, Datetime as DateTime, Date, Time, Bigint, Tinyint, Text, Mediumtext, Longtext, };
use Ulmus\Annotation\Property\Relation\{ Ignore as RelationIgnore };
trait EntityTrait {
use EventTrait;
@ -28,174 +29,18 @@ trait EntityTrait {
* @Ignore
*/
public array $entityLoadedDataset = [];
/**
* @Ignore
*/
public function __get(string $name)
{
$entityResolver = $this->resolveEntity();
# Resolve relations here if one is called
# @TODO REFACTOR THIS CODE ASAP !
if ( $this->isLoaded() ) {
$annotation = $entityResolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?:
$entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation);
if ( $annotation ) {
$vars = [];
$len = strlen( $name ) + 1;
foreach($this->entityDatasetUnmatchedFields as $key => $value) {
if ( substr($key, 0, $len ) === "{$name}$" ) {
$vars[substr($key, $len)] = $value;
}
}
if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) {
$entity = $annotation->entity ?? $entityResolver->properties[$name]['type'];
return $this->$name = ( new $entity() )->fromArray($data)->resetVirtualProperties();
}
}
if ( null !== ( $relation = $entityResolver->searchFieldAnnotation($name, new Relation() ) ) ) {
$order = $entityResolver->searchFieldAnnotationList($name, new OrderBy() );
$where = $entityResolver->searchFieldAnnotationList($name, new Where() );
$filters = $entityResolver->searchFieldAnnotationList($name, new Filter() );
$baseEntity = $relation->entity ?? $relation->bridge ?? $entityResolver->properties[$name]['type'];
$repository = $baseEntity::repository()->open();
foreach($where as $condition) {
$repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [ $this ]) : $condition->value, $condition->operator, $condition->condition);
}
$repository->close();
foreach ($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$applyFilter = function($repository) use ($filters, $name) {
foreach($filters as $filter) {
$repository = call_user_func_array([ $this, $filter->method ], [ $repository, $name ]);
}
return $repository;
};
$field = $relation->key;
$applyFilter = function($repository) use ($filters, $name) {
foreach($filters as $filter) {
$repository = call_user_func_array([ $this, $filter->method ], [ $repository, $name ]);
}
return $repository;
};
switch( $relation->normalizeType() ) {
case 'onetoone':
$repository->limit(1);
if ($relation->foreignKey) {
$repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), is_callable($field) ? $field($this) : $this->$field );
}
$this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository);
$result = call_user_func([$repository, $relation->function]);
if ( count($result) === 0 ) {
return new $baseEntity();
}
return $this->$name = $result[0];
case 'onetomany':
if ($relation->foreignKey) {
$repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), is_callable($field) ? $field($this) : $this->$field );
}
$this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository);
return $this->$name = call_user_func([$applyFilter($repository), $relation->function]);
case 'manytomany':
if ( false === $relation->bridge ?? false ) {
throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' value.");
}
$bridgeEntity = Ulmus::resolveEntity($relation->bridge);
$bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, new Relation() );
$relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, new Relation() );
if ($relationRelation === null) {
throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
}
$repository = $relationRelation->entity()->repository();
$bridgeAlias = uniqid("bridge_");
$relationAlias = uniqid("relation_");
# @TODO Rewrite to be done here, this code must move somewhere else...
$repository->select("{$repository->alias}.*")
->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias)
->join(Query\Join::TYPE_INNER, $this->resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), static::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias)
->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} );
$repository->open();
foreach($where as $condition) {
$repository->where($condition->field, $condition->value, $condition->operator);
}
$repository->close();
foreach($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$this->eventExecute(Event\EntityRelationLoadInterface::class, $name, $repository);
$this->$name = call_user_func([ $applyFilter($repository), $relationRelation->function ]);
if ($relation->bridgeField ?? false) {
$repository = $relationRelation->entity::repository();
$repository->select("$bridgeAlias.*")
->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias)
->join(Query\Join::TYPE_INNER, $this->resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), static::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias)
->where( static::field($bridgeRelation->foreignKey, $relationAlias), $this->{$bridgeRelation->foreignKey} );
$repository->open();
foreach($where as $condition) {
$repository->where($condition->field, $condition->value, $condition->operator);
}
$repository->close();
foreach($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$bridgeName = $relation->bridgeField;
$this->$bridgeName = $repository->collectionFromQuery($relation->bridge);
}
return $this->$name;
}
return;
}
$relation = new Repository\RelationBuilder($this);
if ( false !== $data = $relation->searchRelation($name) ) {
return $this->$name = $data;
}
throw new \Exception(sprintf("[%s] - Undefined variable: %s", static::class, $name));
}
@ -227,7 +72,7 @@ trait EntityTrait {
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_COLUMN_NAME, false) ?? null;
if ( $field === null ) {
$field = $entityResolver->field($key, EntityResolver::KEY_ENTITY_NAME, false);
$field = $entityResolver->field(strtolower($key), EntityResolver::KEY_LC_ENTITY_NAME, false);
}
if ( $field === null ) {
@ -261,7 +106,7 @@ trait EntityTrait {
elseif ( $field['type'] === 'bool' ) {
$this->{$field['name']} = (bool) $value;
}
$this->{$field['name']} = $value;
}
elseif ( ! $field['builtin'] ) {
@ -385,7 +230,7 @@ trait EntityTrait {
*/
public function toCollection() : EntityCollection
{
return static::entityCollection([ $this->toArray() ]);
return static::entityCollection([ $this ]);
}
/**
@ -445,14 +290,6 @@ trait EntityTrait {
return $collection;
}
/**
* @Ignore
*/
public static function queryBuilder() : QueryBuilder
{
return Ulmus::queryBuilder(static::class);
}
/**
* @Ignore
*/

View File

@ -5,7 +5,6 @@ namespace Ulmus\Query;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
class Create extends Fragment {
const SQL_TOKEN = "CREATE TABLE";

View File

@ -6,7 +6,7 @@ abstract class Fragment {
public int $order = 0;
public abstract function render() : string;
public abstract function render() /*: mixed*/;
protected function renderSegments(array $segments, string $glue = " ") : string
{

View File

@ -11,7 +11,7 @@ class GroupBy extends Fragment {
public function set(array $order) : self
{
$this->groupBy = [ $order ];
$this->groupBy = [ $order ];
return $this;
}

View File

@ -14,13 +14,13 @@ class Having extends Fragment {
public array $conditionList;
public QueryBuilder $queryBuilder;
public QueryBuilderInterface $queryBuilder;
public ? Having $parent = null;
public string $condition = Where::CONDITION_AND;
public function __construct(? QueryBuilder $queryBuilder, $condition = Where::CONDITION_AND)
public function __construct(? QueryBuilderInterface $queryBuilder, $condition = Where::CONDITION_AND)
{
$this->queryBuilder = $queryBuilder;
$this->condition = $condition;
@ -70,11 +70,11 @@ class Having extends Fragment {
public string $field;
public string $operator;
public string $condition;
public QueryBuilder $queryBuilder;
public QueryBuilderInterface $queryBuilder;
protected string $content = "";
public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) {
public function __construct(QueryBuilderInterface $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) {
$this->queryBuilder = $queryBuilder;
$this->field = $field;
$this->value = $value;

View File

@ -39,7 +39,7 @@ class Join extends Fragment
public int $order = 40;
public function __construct(QueryBuilder $queryBuilder)
public function __construct(QueryBuilderInterface $queryBuilder)
{
$this->queryBuilder = new QueryBuilder();
$this->queryBuilder->parent = $queryBuilder;

View File

@ -0,0 +1,13 @@
<?php
namespace Ulmus\Query;
interface QueryBuilderInterface
{
public function push(Fragment $queryFragment) : self;
public function pull(Fragment $queryFragment) : self;
public function render(bool $skipToken = false) /* mixed */;
public function reset() : void;
public function getFragment(string $class, int $index = 0) : ? Fragment;
public function removeFragment(Fragment $fragment) : void;
}

View File

@ -10,9 +10,9 @@ class Set extends Fragment {
public array $dataset;
public QueryBuilder $queryBuilder;
public QueryBuilderInterface $queryBuilder;
public function __construct(QueryBuilder $queryBuilder)
public function __construct(QueryBuilderInterface $queryBuilder)
{
$this->queryBuilder = $queryBuilder;
}

View File

@ -2,8 +2,6 @@
namespace Ulmus\Query;
use Ulmus\QueryBuilder;
use Ulmus\Common\EntityField,
Ulmus\Common\Sql;
@ -24,13 +22,13 @@ class Where extends Fragment {
public array $conditionList;
public QueryBuilder $queryBuilder;
public QueryBuilderInterface $queryBuilder;
public ? Where $parent = null;
public string $condition = self::CONDITION_AND;
public function __construct(? QueryBuilder $queryBuilder, $condition = self::CONDITION_AND)
public function __construct(? QueryBuilderInterface $queryBuilder, $condition = self::CONDITION_AND)
{
$this->queryBuilder = $queryBuilder;
$this->condition = $condition;
@ -80,11 +78,11 @@ class Where extends Fragment {
public string $field;
public string $operator;
public string $condition;
public QueryBuilder $queryBuilder;
public QueryBuilderInterface $queryBuilder;
protected string $content = "";
public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) {
public function __construct(QueryBuilderInterface $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) {
$this->queryBuilder = $queryBuilder;
$this->field = $field;
$this->value = $value;

View File

@ -2,13 +2,15 @@
namespace Ulmus;
class QueryBuilder
use Ulmus\Query\QueryBuilderInterface;
class QueryBuilder implements Query\QueryBuilderInterface
{
public Query\Where $where;
public Query\Having $having;
public QueryBuilder $parent;
public QueryBuilderInterface $parent;
/**
* Those are the parameters we are going to bind to PDO.
@ -371,7 +373,7 @@ class QueryBuilder
return array_shift($this->queryStack);
}
public function render(bool $skipToken = false) : string
public function render(bool $skipToken = false) /* : mixed */
{
$sql = [];
@ -396,11 +398,13 @@ class QueryBuilder
unset($this->where, $this->having);
}
public function getFragment(string $class) : ? Query\Fragment
public function getFragment(string $class, int $index = 0) : ? Query\Fragment
{
foreach($this->queryStack as $item) {
if ( get_class($item) === $class ) {
return $item;
if ( $index-- === 0 ) {
return $item;
}
}
}

View File

@ -2,7 +2,7 @@
namespace Ulmus;
use Ulmus\Annotation\Property\{ Where, Having, Relation, Join };
use Ulmus\Annotation\Property\{ Where, Having, Relation, Join, WithJoin, Relation\Ignore as RelationIgnore };
use Ulmus\Common\EntityResolver;
class Repository
@ -13,7 +13,7 @@ class Repository
public ? ConnectionAdapter $adapter;
protected QueryBuilder $queryBuilder;
protected Query\QueryBuilderInterface $queryBuilder;
protected EntityResolver $entityResolver;
@ -271,6 +271,39 @@ class Repository
}
}
public function selectEntity(string $entity, string $alias, string $prependField = "") : self
{
$prependField and ( $prependField .= "$" );
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) {
$this->select("$alias.$key as {$prependField}{$field['name']}");
}
}
return $this;
}
public function selectJsonEntity(string $entity, string $alias, bool $skipFrom = false) : self
{
$fieldlist = [];
foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if (null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore)) {
$fieldlist[] = $key;
$fieldlist[] = $entity::field($field['name'], $alias);
}
}
$this->select(
Common\Sql::function('JSON_ARRAYAGG', Common\Sql::function('JSON_OBJECT', ... $fieldlist))
);
return $skipFrom ? $this : $this->from(
$entity::resolveEntity()->tableName(), $alias, $entity::resolveEntity()->schemaName()
);
}
public function select(/*array|Stringable*/ $fields) : self
{
$this->queryBuilder->select($fields);
@ -444,8 +477,8 @@ class Repository
$this->select("{$this->alias}.*");
}
# Apply FILTER annotation to this too !
foreach((array) $fields as $item) {
# @TODO Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) {
$annotation = $this->entityResolver->searchFieldAnnotation($item, new Join) ?:
$this->entityResolver->searchFieldAnnotation($item, new Relation);
@ -459,7 +492,9 @@ class Repository
$entity = $annotation->entity ?? $this->entityResolver->properties[$item]['type'];
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
$this->select("$alias.$key as {$alias}\${$field['name']}");
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) {
$this->select("$alias.$key as {$alias}\${$field['name']}");
}
}
$this->open();
@ -482,9 +517,14 @@ class Repository
$this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias) {
foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) {
$field = clone $condition->field;
if ( ! is_object($condition->field) ) {
$field = $this->entityClass::field($condition->field);
}
else {
$field = clone $condition->field;
}
if ( is_object($condition->field) && ( $condition->field->entityClass === $entity ) ) {
if ( $condition->field->entityClass === $entity ) {
$field->alias = $alias;
$join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->value, $condition->operator);
@ -500,13 +540,77 @@ class Repository
return $this;
}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self
public function withSubquery(/*string|array*/ $fields) : self
{
$searchRequest->count = $searchRequest->filter( clone $this )
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->groups($searchRequest->groups())
->count();
# We skip subqueries when counting results since it should not affect the row count.
if ( $this instanceof Repository\ServerRequestCountRepository ) {
return $this;
}
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->select("{$this->alias}.*");
}
# Apply FILTER annotation to this too !
foreach(array_filter((array) $fields) as $item) {
if ( $relation = $this->entityResolver->searchFieldAnnotation($item, new Relation) ) {
$alias = $relation->alias ?? $item;
if ( $relation->isManyToMany() ) {
$entity = $relation->bridge;
$repository = $relation->bridge::repository();
$repository->queryBuilder->parent = $this->queryBuilder;
extract(Repository\RelationBuilder::relationAnnotations($item, $relation));
$repository->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $relation->bridgeField), $relationRelation->entity::field($relationRelation->foreignKey, $alias), $relation->bridgeField)
->where( $entity::field($bridgeRelation->key, $relation->bridgeField), $entity::field($bridgeRelation->foreignKey, $this->alias))
->selectJsonEntity($relationRelation->entity, $alias)->open();
}
else {
$entity = $relation->entity ?? $this->entityResolver->properties[$item]['type'];
$repository = $entity::repository()->selectJsonEntity($entity, $alias)->open();
}
# $relation->isManyToMany() and $repository->selectJsonEntity($relation->bridge, $relation->bridgeField, true);
foreach($this->entityResolver->searchFieldAnnotationList($item, new Where() ) as $condition) {
$repository->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator);
}
foreach($this->entityResolver->searchFieldAnnotationList($item, new Having() ) as $condition) {
$repository->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->value, $condition->operator);
}
$repository->close();
$key = is_string($relation->key) ? $this->entityClass::field($relation->key) : $relation->key;
if (! $relation->isManyToMany() ) {
$foreignKey = is_string($relation->foreignKey) ? $entity::field($relation->foreignKey, $alias) : $relation->foreignKey;
$repository->where( $foreignKey, $key);
}
$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.");
}
}
return $this;
}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{
if ($count) {
$searchRequest->count = $searchRequest->filter(new Repository\ServerRequestCountRepository($this->entityClass, $this->alias, $this->adapter))
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->groups($searchRequest->groups())
->count();
}
return $searchRequest->filter($this)
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
@ -551,18 +655,18 @@ class Repository
return Ulmus::runQuery($this->queryBuilder, $this->adapter);
}
public function resetQuery() : self
{
$this->queryBuilder->reset();
return $this;
}
protected function insertSqlQuery(array $dataset) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Insert::class) ) {
$this->insert(array_keys($dataset), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
$this->insert(array_map([ $this, 'escapeField' ] , array_keys($dataset)), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
}
$this->values($dataset);
@ -651,6 +755,11 @@ class Repository
return new $this->entityClass();
}
public function escapeField(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD);
}
public function escapeTable(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE);

View File

@ -0,0 +1,42 @@
<?php
namespace Ulmus\Repository;
use Ulmus\{ Repository, Query };
class MssqlRepository extends Repository {
protected function finalizeQuery() : void
{
if ( null !== $offset = $this->queryBuilder->getFragment(Query\Offset::class) ) {
if ( null === $limit = $this->queryBuilder->getFragment(Query\Limit::class) ) {
throw new \Exception("Your offset query fragment is missing a LIMIT value.");
}
# an order by is mandatory for mssql offset/limit
if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) {
$this->orderBy("(SELECT 0)");
}
$mssqlOffset = new \Ulmus\Query\MsSQL\Offset();
$mssqlOffset->set($offset->offset, $limit->limit);
$this->queryBuilder->removeFragment($offset);
$this->queryBuilder->removeFragment($limit);
$this->queryBuilder->push($mssqlOffset);
}
elseif ( null !== $limit = $this->queryBuilder->getFragment(Query\Limit::class) ) {
if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) {
$select->top = $limit->limit;
}
elseif ( null !== $delete = $this->queryBuilder->getFragment(Query\Delete::class) ) {
$delete->top = $limit->limit;
}
$this->queryBuilder->removeFragment($limit);
}
}
}

View File

@ -0,0 +1,259 @@
<?php
namespace Ulmus\Repository;
use Ulmus\{ Ulmus, Annotation, Query, Common, Common\EntityResolver, Repository, Event, EntityCollection, };
use Ulmus\Annotation\Property\{Filter, OrderBy, Relation, Relation\Ignore as RelationIgnore, Where, WithJoin, };
use Closure;
class RelationBuilder
{
protected Repository $repository;
protected /*object|string*/ $entity;
protected EntityResolver $resolver;
protected array $orders;
protected array $wheres;
protected array $filters;
protected array $joins;
public function __construct(/*string|object*/ $entity, ? Repository $repository = null)
{
$this->entity = $entity;
$this->resolver = $entity::resolveEntity();
if ($repository) {
$this->repository = $repository;
}
}
public function searchRelation(string $name) /* : object|EntityCollection|bool */
{
# Resolve relations here if one is called
if ( $this->entity->isLoaded() ) {
if ( false !== ( $dataset = $this->fetchFromDataset($name) ) ) {
return $dataset;
}
return $this->resolveRelation($name);
}
}
protected function resolveRelation(string $name) /* : object|EntityCollection */
{
if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, new Relation() ) ) ) {
$this->orders = $this->resolver->searchFieldAnnotationList($name, new OrderBy() );
$this->wheres = $this->resolver->searchFieldAnnotationList($name, new Where() );
$this->filters = $this->resolver->searchFieldAnnotationList($name, new Filter() );
$this->joins = $this->resolver->searchFieldAnnotationList($name, new WithJoin() );
switch( true ) {
case $relation->isOneToOne():
if ( $relation->hasBridge() ) {
# @TODO ! dump($this->relationAnnotations($name, $relation));
}
else {
$this->oneToOne($name, $relation);
}
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
$result = call_user_func([ $this->repository, $relation->function ]);
return count($result) === 0 ? $this->instanciateEmptyEntity($name, $relation): $result[0];
case $relation->isOneToMany():
$this->oneToMany($name, $relation);
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
return call_user_func([ $this->repository, $relation->function ]);
case $relation->isManyToMany():
$this->manyToMany($name, $relation, $relationRelation);
$this->entity->eventExecute(Event\EntityRelationLoadInterface::class, $name, $this->repository);
$results = call_user_func([ $this->repository, $relationRelation->function ]);
if ($relation->bridgeField ?? false) {
$collection = $relation->bridge::entityCollection();
foreach($results as $entity) {
$collection->append($this->fetchFromDataset($relation->bridgeField, $entity->entityLoadedDataset));
}
$this->entity->{$relation->bridgeField} = $collection;
}
return $results;
}
}
return false;
}
protected function applyFilter(Repository $repository, string $name) : Repository
{
foreach($this->filters ?? [] as $filter) {
$repository = call_user_func_array([ $this->entity, $filter->method ], [ $repository, $name ]);
}
return $repository;
}
protected function applyWhere() : void
{
if ($this->wheres) {
$this->repository->open();
foreach($this->wheres as $condition) {
$this->repository->where($condition->field, $condition->value, $condition->operator);
}
$this->repository->close();
}
}
protected function applyOrderBy() : void
{
foreach($this->orders as $item) {
$this->repository->orderBy($item->field, $item->order);
}
}
protected function instanciateEmptyEntity(string $name, Relation $relation) : object
{
$class = $relation->entity ?? $this->resolver->properties[$name]['type'];
return new $class();
}
protected function fetchFromDataset($name, ? array $data = null) /* object|bool */
{
$annotation = $this->resolver->searchFieldAnnotation($name, new Annotation\Property\Join) ?:
$this->resolver->searchFieldAnnotation($name, new Annotation\Property\Relation);
if ( $annotation ) {
$vars = [];
$len = strlen( $name ) + 1;
if ( ( $annotation instanceof Relation ) && $annotation->isManyToMany() ) {
$entity = $this->relationAnnotations($name, $annotation)['relationRelation']->entity;
}
else {
$entity = $annotation->entity ?? $this->resolver->properties[$name]['type'];
}
foreach($data ?: $this->entity->entityLoadedDataset as $key => $value) {
if ( $key === "{$name}\$collection" ) {
if ($value) {
if ( null === ( $dataset = \json_decode($value, true) ) ) {
throw new \Exception(sprintf("JSON error '%s' from '%s'", \json_last_error_msg(), $value));
}
return $entity::entityCollection()->fromArray($dataset)->iterate(fn($e) => $e->resetVirtualProperties());
}
else {
return $entity::entityCollection();
}
}
elseif ( substr($key, 0, $len ) === "{$name}\$" ) {
$vars[substr($key, $len)] = $value;
}
}
if ($vars) {
if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) {
return ( new $entity() )->fromArray($data)->resetVirtualProperties();
}
else {
return new $entity();
}
}
}
return false;
}
public function oneToOne(string $name, Relation $relation) : Repository
{
return $this->oneToMany($name, $relation)->limit(1);
}
public function oneToMany(string $name, Relation $relation) : Repository
{
$baseEntity = $relation->entity ?? $this->resolver->properties[$name]['type'];
$this->repository = $baseEntity::repository();
$this->applyWhere();
$this->applyOrderBy();
$field = $relation->key;
if ($relation->foreignKey) {
$this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), ! is_string($field) && is_callable($field) ? $field($this->entity) : $this->entity->$field );
}
return $this->applyFilter($this->repository, $name);
}
public function manyToMany(string $name, Relation $relation, ? Relation & $relationRelation = null, bool $selectBridgeField = true) : Repository
{
extract($this->relationAnnotations($name, $relation));
$this->repository = $relationRelation->entity::repository();
$bridgeAlias = $relation->bridgeField ?? uniqid("bridge_");
$relationAlias = $relation->field;
$this->repository->select("{$this->repository->alias}.*")
->join(Query\Join::TYPE_INNER, $bridgeEntity->tableName(), $relation->bridge::field($relationRelation->key, $bridgeAlias), $relationRelation->entity::field($relationRelation->foreignKey), $bridgeAlias, function($join) {
})->join(Query\Join::TYPE_INNER, $this->entity::resolveEntity()->tableName(), $relation->bridge::field($bridgeRelation->key, $bridgeAlias), $this->entity::field($bridgeRelation->foreignKey, $relationAlias), $relationAlias, function($join) {
})->where( $this->entity::field($bridgeRelation->foreignKey, $relationAlias), is_string($this->entity) ? $this->entity::field($bridgeRelation->foreignKey) : $this->entity->{$bridgeRelation->foreignKey} );
if ($selectBridgeField && $relation->bridgeField) {
$this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias);
}
return $this->applyFilter($this->repository, $name);
}
public static function relationAnnotations(string $name, Relation $relation) : array
{
if ( $relation->isOneToOne() || $relation->isManyToMany() ) {
if ( ! $relation->hasBridge() ) {
throw new \Exception("Your many-to-many @Relation() from variable `$name` is missing a 'bridge' field.");
}
$bridgeEntity = Ulmus::resolveEntity($relation->bridge);
$bridgeRelation = $bridgeEntity->searchFieldAnnotation($relation->field, new Relation() );
$relationRelation = $bridgeEntity->searchFieldAnnotation($relation->foreignField, new Relation() );
if ($relationRelation === null) {
throw new \Exception("@Relation annotation not found for field `{$relation->foreignField}` in entity {$relation->bridge}");
}
$relationRelation->entity ??= $relation->bridge::resolveEntity()->properties[$relation->foreignField]['type'];
return [
'bridgeEntity' => $bridgeEntity,
'bridgeRelation' => $bridgeRelation,
'relationRelation' => $relationRelation,
];
}
return [];
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Repository;
use Ulmus\Repository;
class ServerRequestCountRepository extends Repository {}

View File

@ -13,6 +13,8 @@ trait SearchRequestPaginationTrait {
public int $limit = 25;
public bool $skipCount = false;
public ? array $columns = null;
public function limit(): int
{

View File

@ -18,13 +18,15 @@ abstract class Ulmus
public static array $resolved = [];
public static function iterateQueryBuilder(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) : Generator
public static function iterateQueryBuilder(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) : Generator
{
$sql = $queryBuilder->render();
$rendered = $queryBuilder->render();
$statement = ( $adapter ?: static::$defaultAdapter )->pdo()->select($sql, $queryBuilder->parameters ?? []);
$statement = ( $adapter ?: static::$defaultAdapter )->connector()->select($rendered, $queryBuilder->parameters ?? []);
while ( $row = $statement->fetch() ) {
$i = 0;
while ( $row = $statement->fetch() ) {
yield $row;
}
@ -37,13 +39,13 @@ abstract class Ulmus
];
}
public static function datasetQueryBuilder(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) : array
public static function datasetQueryBuilder(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) : array
{
$rows = [];
$rendered = $queryBuilder->render();
$sql = $queryBuilder->render();
$statement = ( $adapter ?: static::$defaultAdapter )->pdo()->select($sql, $queryBuilder->parameters ?? []);
$statement = ( $adapter ?: static::$defaultAdapter )->connector()->select($rendered, $queryBuilder->parameters ?? []);
while ( $row = $statement->fetch() ) {
$rows[] = $row;
@ -56,22 +58,22 @@ abstract class Ulmus
return $rows;
}
public static function pdo(? ConnectionAdapter $adapter = null) : Common\PdoObject
public static function connector(? ConnectionAdapter $adapter = null) : object
{
return ( $adapter ?: static::$defaultAdapter )->pdo();
return ( $adapter ?: static::$defaultAdapter )->connector();
}
public static function runSelectQuery(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null)
public static function runSelectQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null)
{
$dataset = static::pdo($adapter)->select($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? []));
$dataset = static::connector($adapter)->select($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? []));
$queryBuilder->reset();
return $dataset;
}
public static function runQuery(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null)
public static function runQuery(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null)
{
$return = static::pdo($adapter)->runQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? []));
$return = static::connector($adapter)->runQuery($queryBuilder->render(), array_merge($queryBuilder->values ?? [], $queryBuilder->parameters ?? []));
$queryBuilder->reset();
return $return;
@ -87,7 +89,7 @@ abstract class Ulmus
return new static::$repositoryClass(...$arguments);
}
public static function queryBuilder(...$arguments) : QueryBuilder
public static function queryBuilder(...$arguments) : Query\QueryBuilderInterface
{
return new static::$queryBuilderClass(...$arguments);
}
@ -116,9 +118,9 @@ abstract class Ulmus
static::$registeredAdapters[$adapter->name] = $adapter;
}
protected static function fetchQueryBuilder(QueryBuilder $queryBuilder, ? ConnectionAdapter $adapter = null) : array
protected static function fetchQueryBuilder(Query\QueryBuilderInterface $queryBuilder, ? ConnectionAdapter $adapter = null) : array
{
$result = ( $adapter ?: static::$defaultAdapter )->pdo->select($queryBuilder->render(), $queryBuilder->parameters ?? [])->fetchAll();
$result = ( $adapter ?: static::$defaultAdapter )->connector->select($queryBuilder->render(), $queryBuilder->parameters ?? [])->fetchAll();
$queryBuilder->reset();
return $result;