- A lot of work done on bug fixing. Added a new SQLite adapter

This commit is contained in:
Dave M. 2022-01-28 16:37:35 +00:00
parent 667df92e52
commit c14644cdda
28 changed files with 727 additions and 234 deletions

View File

@ -3,6 +3,7 @@
namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject;
use Ulmus\Migration\FieldDefinition;
interface AdapterInterface {
public const IDENTIFIER_FIELD = 1;
@ -10,10 +11,16 @@ interface AdapterInterface {
public const IDENTIFIER_DATABASE = 3;
public const IDENTIFIER_SCHEMA = 4;
public const IDENTIFIER_VALUE = 5;
public function connect() : object /* | PdoObject|mixed */;
public function buildDataSourceName() : string;
public function setup(array $configuration) : void;
public function escapeIdentifier(string $segment, int $type) : string;
public function defaultEngine() : ? string;
public function databaseName() : string;
public function mapFieldType(FieldDefinition $field) : string;
public function schemaTable(string $databaseName, string $tableName) /*: object|EntityCollection */;
public function repositoryClass() : string;
public function queryBuilderClass() : string;
public function tableSyntax() : array;
}

View File

@ -0,0 +1,95 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\{Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder};
trait DefaultAdapterTrait
{
public function repositoryClass() : string
{
return Repository::class;
}
public function queryBuilderClass() : string
{
return QueryBuilder::class;
}
public function tableSyntax() : array
{
return [
'ai' => "AUTO_INCREMENT",
'pk' => "PRIMARY KEY",
'unsigned' => "UNSIGNED",
];
}
public function databaseName() : string
{
return $this->database;
}
public function schemaTable(string $databaseName, string $tableName) /* : ? object */
{
return Table::repository()->where(Table::field('schema'), $databaseName)->loadOneFromField(Table::field('name'), $tableName);
}
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$type = $field->type;
$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 . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" );
}
}

View File

@ -5,8 +5,13 @@ namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject;
use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Migration\FieldDefinition;
use Ulmus\{Entity\InformationSchema\Table, Repository, QueryBuilder};
class MsSQL implements AdapterInterface {
use DefaultAdapterTrait;
const DSN_PREFIX = "sqlsrv";
public int $port;
@ -129,7 +134,7 @@ class MsSQL implements AdapterInterface {
$parts[] = "WSID={$this->wsid}";
}
return "sqlsrv:" . implode(';', $parts);
return static::DSN_PREFIX . ":" . implode(';', $parts);
}
public function setup(array $configuration) : void

View File

@ -2,11 +2,18 @@
namespace Ulmus\Adapter;
use Ulmus\Entity\InformationSchema\Table;
use Ulmus\QueryBuilder;
use Ulmus\Repository;
use Ulmus\Common\PdoObject;
use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Migration\FieldDefinition;
class MySQL implements AdapterInterface {
use DefaultAdapterTrait;
const DSN_PREFIX = "mysql";
public string $hostname;
@ -17,7 +24,7 @@ class MySQL implements AdapterInterface {
public string $password;
public string $charset = "utf8mb4";
public ? string $socket;
public int $port = 3306;
@ -68,6 +75,9 @@ class MySQL implements AdapterInterface {
catch(PDOException $ex){
throw $ex;
}
finally {
$this->password = str_repeat('*', strlen($this->password));
}
return $pdo;
}
@ -91,7 +101,7 @@ class MySQL implements AdapterInterface {
$parts[] = "charset={$this->charset}";
}
return "mysql:" . implode(';', $parts);
return static::DSN_PREFIX . ":" . implode(';', $parts);
}
public function setup(array $configuration) : void

140
src/Adapter/SQLite.php Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject;
use Ulmus\Entity\Sqlite\Table;
use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Migration\FieldDefinition;
use Ulmus\{ Repository, QueryBuilder };
class SQLite implements AdapterInterface {
use DefaultAdapterTrait;
const DSN_PREFIX = "sqlite";
public string $path;
public array $pragma;
public function __construct(
? string $path = null,
? array $pragma = null
) {
if ($path !== null) {
$this->path = $path;
}
if ($pragma !== null) {
$this->pragma = $pragma;
}
}
public function connect() : PdoObject
{
try {
$pdo = new PdoObject($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);
}
catch(PDOException $ex){
throw $ex;
}
return $pdo;
}
public function buildDataSourceName() : string
{
$parts[] = $this->path;
return static::DSN_PREFIX . ":" . implode(';', $parts);
}
public function setup(array $configuration) : void
{
$this->path = $configuration['path'] ?? "";
$this->pragma = $configuration['pragma'] ?? [];
}
# https://sqlite.org/lang_keywords.html
public function escapeIdentifier(string $segment, int $type) : string
{
switch($type) {
case static::IDENTIFIER_DATABASE:
case static::IDENTIFIER_TABLE:
case static::IDENTIFIER_FIELD:
return '"' . trim(str_replace('"', '""', $segment), '"') . '"';
case static::IDENTIFIER_VALUE:
return "'$segment'";
}
}
public function defaultEngine(): ? string
{
return null;
}
public function databaseName() : string
{
$base = basename($this->path);
return substr($base, 0, strrpos($base, '.') ?: strlen($base));
}
public function schemaTable(string $databaseName, string $tableName) /* : ? object */
{
return Table::repository()->loadOneFromField(Table::field('tableName'), $tableName);
}
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) ) {
$type = "TEXT";
$length = strlen((string) $type);
}
else {
switch($type) {
case "bool":
$check = sprintf("CHECK (%s IN (0, 1))", $field->getColumnName());
case "bigint":
case "int":
$type = "INTEGER";
break;
case "array":
case "string":
$type = "TEXT";
$length = null;
break;
case "float":
$type = "REAL";
break;
default:
$type = "BLOB";
break;
}
}
return $typeOnly ? $type : $type . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" );
}
public function tableSyntax() : array
{
return [
'ai' => "AUTOINCREMENT",
'pk' => "PRIMARY KEY",
'unsigned' => "",
];
}
}

View File

@ -50,9 +50,9 @@ class EntityField implements WhereRawParameter
return false;
}
public static function generateCreateColumn($field) : string
public static function generateCreateColumn(AdapterInterface $adapter, $field) : string
{
$definition = new FieldDefinition($field);
$definition = new FieldDefinition($adapter, $field);
return implode(" ", [
$definition->getSqlName(),

View File

@ -164,7 +164,7 @@ class EntityResolver {
public function databaseName() : ? string
{
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->database ?? null;
return $this->tableAnnotation(false)->database ?? $this->databaseAdapter()->adapter()->databaseName() ?? null;
}
public function sqlAdapter() : \Ulmus\ConnectionAdapter

View File

@ -37,6 +37,8 @@ class ConnectionAdapter
}
$this->adapter->setup($connection);
unset($this->configuration['connections'][$this->name]);
}
public function getConfiguration() : array
@ -77,7 +79,7 @@ class ConnectionAdapter
*/
protected function instanciateAdapter($name) : AdapterInterface
{
$class = substr($name, 0, 2) === "\\" ? $name : "\\Ulmus\\Adapter\\$name";
$class = substr($name, 0, 2) === "\\" ? $name : sprintf("\\%s\\Adapter\\%s", __NAMESPACE__, $name);
return new $class();
}

View File

@ -0,0 +1,43 @@
<?php
namespace Ulmus\Entity\Sqlite;
use Ulmus\EntityCollection;
/**
* @Table('name' => "sqlite_master")
*/
class Schema
{
use \Ulmus\EntityTrait;
/**
* @Id
*/
public ? string $name;
/**
* @Field
*/
public ? string $type;
/**
* @Field('name' => 'tbl_name')
*/
public ? string $tableName;
/**
* @Field
*/
public ? int $rootpage;
/**
* @Field
*/
public ? string $sql;
/**
* @Relation('oneToMany', 'key' => 'tableName', 'foreignKey' => 'tableName', 'entity' => Schema::class)
*/
public EntityCollection $columns;
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ulmus\Entity\Sqlite;
use Ulmus\Repository;
class Table extends Schema
{
public static function repository(string $alias = Repository::DEFAULT_ALIAS): Repository
{
return new class(static::class, $alias) extends Repository\SqliteRepository
{
public function finalizeQuery(): void
{
$this->select(Table::field('tableName'))->groupBy(Table::field('tableName'));
}
};
}
}

View File

@ -146,7 +146,12 @@ class EntityCollection extends \ArrayObject {
return $this->filtersCollection(fn($obj) => is_a($obj, $className));
}
public function column($field, bool $unique = false) : array
public function column($field, bool $uniqueValue = false, $keyField = null) : array
{
return $this->map($field, $keyField, $uniqueValue);
}
public function map($field, $keyField = null, bool $uniqueValue = false) : array
{
$list = [];
@ -158,11 +163,23 @@ class EntityCollection extends \ArrayObject {
$value = $item->$field;
}
if ($unique && in_array($value, $list, true)) {
if ($uniqueValue && in_array($value, $list, true)) {
continue;
}
$list[] = $value;
if ( $keyField !== null ) {
if ( is_callable($keyField) ) {
$key = call_user_func_array($keyField, [ $item ]);
}
else {
$key = $item->$keyField;
}
$list[$key] = $value;
}
else {
$list[] = $value;
}
}
return $list;

View File

@ -2,6 +2,7 @@
namespace Ulmus\Migration;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation\Property\Field;
use Ulmus\Entity;
@ -25,8 +26,12 @@ class FieldDefinition {
public ? string $update;
public function __construct(array $data)
public AdapterInterface $adapter;
public function __construct(AdapterInterface $adapter, array $data)
{
$this->adapter = $adapter;
$this->name = $data['name'];
$this->builtIn = $data['builtin'];
$this->tags = $data['tags'];
@ -46,108 +51,56 @@ class FieldDefinition {
public function getSqlType(bool $typeOnly = false) : string
{
$type = $this->type;
$length = $this->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 . ( $length ? "($length" . ( $precision ? ",$precision" : "" ) . ")" : "" );
return $this->adapter->mapFieldType($this, $typeOnly);
}
public function getSqlParams() : string
{
$default = $this->getDefault();
$syntax = $this->adapter->tableSyntax();
return implode(' ', array_filter([
$this->isUnsigned() ? "UNSIGNED" : null,
$this->isUnsigned() ? $syntax['unsigned'] : null,
$this->nullable ? "NULL" : "NOT NULL",
$default ? "DEFAULT $default" : null,
$this->isAutoIncrement() ? "AUTO_INCREMENT" : null,
$this->isPrimaryKey() ? "PRIMARY KEY" : null,
$this->isPrimaryKey() ? $syntax['pk'] : null,
$this->isAutoIncrement() ? $syntax['ai'] : null,
]));
}
protected function getFieldTag() : ? Field
public function getFieldTag() : ? Field
{
$field = array_filter($this->tags, fn($item) => $item['object'] instanceof Field);
return array_pop($field)['object'];
}
protected function getColumnName() : ? string
public function getColumnName() : ? string
{
return $this->getFieldTag()->name ?? $this->name;
}
protected function getDefault() : ? string
public function getDefault() : ? string
{
return $this->getFieldTag()->attributes['default'] ?? ( $this->nullable ? "NULL" : null ) ;
}
protected function isPrimaryKey() : bool
public function isPrimaryKey() : bool
{
return $this->getFieldTag()->attributes['primary_key'] ?? false;
}
protected function isAutoIncrement() : bool
public function isAutoIncrement() : bool
{
return $this->getFieldTag()->attributes['auto_increment'] ?? false;
}
protected function isUnique() : bool
public function isUnique() : bool
{
return $this->getFieldTag()->attributes['unique'] ?? false;
}
protected function isUnsigned() : bool
public function isUnsigned() : bool
{
return $this->getFieldTag()->attributes['unsigned'] ?? false;
}

View File

@ -1,21 +0,0 @@
<?php
namespace Ulmus\Modeler;
class Schema {
public function __construct()
{
}
public function compare()
{
}
public function migrate()
{
}
}

View File

@ -2,6 +2,7 @@
namespace Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
@ -17,7 +18,13 @@ class Alter extends Fragment {
public bool $skipExisting = true;
public array $fieldList;
public array $fieldList;
public AdapterInterface $adapter;
public function __construct(AdapterInterface $adapter) {
$this->adapter = $adapter;
}
public function render() : string
{
@ -29,7 +36,7 @@ class Alter extends Fragment {
public function renderFields() : string
{
return "(" . PHP_EOL . implode("," . PHP_EOL, array_map(function($field) {
return " " . EntityField::generateAlterColumn($field);
return " " . EntityField::generateAlterColumn($this->adapter, $field);
}, $this->fieldList)) . PHP_EOL . ")";
}
}

View File

@ -2,6 +2,7 @@
namespace Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Annotation,
Ulmus\Common\EntityField;
@ -19,6 +20,12 @@ class Create extends Fragment {
public array $fieldList;
public AdapterInterface $adapter;
public function __construct(AdapterInterface $adapter) {
$this->adapter = $adapter;
}
public function render() : string
{
return $this->renderSegments([
@ -29,7 +36,7 @@ class Create extends Fragment {
public function renderFields() : string
{
return "(" . PHP_EOL . implode("," . PHP_EOL, array_map(function($field) {
return " " . EntityField::generateCreateColumn($field);
return " " . EntityField::generateCreateColumn($this->adapter, $field);
}, $this->fieldList)) . PHP_EOL . ")";
}
}

View File

@ -8,7 +8,7 @@ use Ulmus\Repository\ConditionTrait;
class Join extends Fragment
{
use ConditionTrait;
const SQL_OUTER = "OUTER";
const SQL_TOKEN = "JOIN";
@ -39,9 +39,12 @@ class Join extends Fragment
public int $order = 40;
public int $joinOrder = 0;
public function __construct(QueryBuilderInterface $queryBuilder)
{
$this->queryBuilder = new QueryBuilder();
$cls = get_class($queryBuilder);
$this->queryBuilder = new $cls();
$this->queryBuilder->parent = $queryBuilder;
}

View File

@ -42,7 +42,6 @@ class Select extends Fragment {
if ( ! is_numeric($key) ) {
$value = sprintf(static::FIELD_AS, $value, $key);
}
}
return $this->renderSegments([

View File

@ -0,0 +1,40 @@
<?php
namespace Ulmus\Query\Sqlite;
class Pragma extends \Ulmus\Query\Fragment {
const SQL_TOKEN = "PRAGMA %s%s";
public int $order = 15;
public /* object|string */ $pragma;
public $value;
public bool $callable;
public function set(/* object|Stringable */ $pragma, /* ? mixed */ $value = null, bool $callable = false) : self
{
$this->pragma = $pragma;
if ($value) {
$this->value = $value;
}
$this->callable = $callable;
return $this;
}
public function render() : string
{
if ( isset($this->value) ) {
$value = sprintf($this->callable ? " (%s)" : " = %s", $this->value);
}
return $this->renderSegments([
sprintf(static::SQL_TOKEN, $this->pragma, $value ?? "")
]);
}
}

View File

@ -305,7 +305,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
$join->outer = $outer;
$join->alias = $alias;
return $join;
}
@ -328,7 +328,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
return $this;
}
public function create(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
public function create(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
if ( null === $this->getFragment(Query\Create::class) ) {
if ( $schema ) {
@ -339,7 +339,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
$table = "$database.$table";
}
$create = new Query\Create();
$create = new Query\Create($adapter);
$this->push($create);
$create->fieldList = $fieldlist;
@ -352,7 +352,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
return $this;
}
public function alter(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
public function alter(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
if ( null === $this->getFragment(Query\Create::class) ) {
if ( $schema ) {
@ -363,7 +363,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
$table = "$database.$table";
}
$alter = new Query\Alter();
$alter = new Query\Alter($adapter);
$this->push($alter);
$alter->fieldList = $fieldlist;
@ -405,7 +405,7 @@ class QueryBuilder implements Query\QueryBuilderInterface
$sql = [];
usort($this->queryStack, function($q1, $q2) {
return $q1->order <=> $q2->order;
return (float) $q1->order <=> (float) $q2->order;
});
foreach($this->queryStack as $fragment) {
@ -456,9 +456,9 @@ class QueryBuilder implements Query\QueryBuilderInterface
}
}
public function getFragments() : array
public function getFragments(/*? Query\Fragment|array*/ $fragment = null) : array
{
return $this->queryStack;
return $fragment === null ? array_filter($this->queryStack, fn($e) => get_class($e) === $fragment) : $this->queryStack;
}
public function __toString() : string
@ -485,4 +485,15 @@ class QueryBuilder implements Query\QueryBuilderInterface
{
$this->values = $values;
}
protected function nextJoinOrder() : float
{
$next = 0;
foreach($this->getFragments(Query\Join::class) as $join) {
$next = max($next, $join->joinOrder - Query\Join::ORDER_VALUE);
}
return $next + 1;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Ulmus\QueryBuilder;
use Ulmus\QueryBuilder;
use Ulmus\Query;
class MssqlQueryBuilder extends QueryBuilder implements Ulmus\Query\QueryBuilderInterface
{
public function limit(int $value) : self
{
if ( null === $offset = $this->getFragment(Query\MsSQL\Offset::class) ) {
$offset = new Query\MsSQL\Offset();
$this->push($offset);
}
$offset->limit = $value;
return $this;
}
public function offset(int $value) : self
{
if ( null === $offset = $this->getFragment(Query\MsSQL\Offset::class) ) {
$offset = new Query\MsSQL\Offset();
$this->push($offset);
}
$offset->offset = $value;
return $this;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Ulmus\QueryBuilder;
use Ulmus\QueryBuilder;
use Ulmus\Query;
class SqliteQueryBuilder extends QueryBuilder implements Query\QueryBuilderInterface
{
public function pragma(/*object|Stringable*/ $name, $value = null, bool $callable = false) : self
{
if ( null !== ( $pragma = $this->getFragment(Query\Sqlite\Pragma::class) ) ) {
$pragma->set($name, $value, $callable);
}
else {
$pragma = new Query\Sqlite\Pragma();
$pragma->set($name, $value, $callable);
$this->push($pragma);
}
return $this;
}
}

View File

@ -7,7 +7,7 @@ use Ulmus\Common\EntityResolver;
class Repository
{
use EventTrait, Repository\ConditionTrait;
use EventTrait, Repository\ConditionTrait, Repository\EscapeTrait;
const DEFAULT_ALIAS = "this";
@ -28,9 +28,9 @@ class Repository
$this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity);
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
$this->queryBuilder = new QueryBuilder();
$this->queryBuilder = Ulmus::queryBuilder($entity);
}
public function __clone()
{
#$this->queryBuilder = clone $this->queryBuilder;
@ -148,9 +148,9 @@ class Repository
# $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH);
$statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery();
if ( ( 0 !== $statement->lastInsertId ) &&
( null !== $primaryKeyDefinition )) {
if ( ( 0 !== $statement->lastInsertId ) &&
( null !== $primaryKeyDefinition )) {
$pkField = key($primaryKeyDefinition);
$dataset[$pkField] = $statement->lastInsertId;
@ -195,74 +195,20 @@ class Repository
}
public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void
{
foreach ((array)$fields as $name) {
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) {
$relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type));
$order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy());
$where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where());
$baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type'];
$baseEntityResolver = $baseEntity::resolveEntity();
$property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name'];
$entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name'];
$repository = $baseEntity::repository();
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);
}
foreach ($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$field = $relation->key;
$values = [];
$key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey);
foreach ($collection as $item) {
$values[] = is_callable($field) ? $field($item) : $item->$entityProperty;
}
$repository->where($key, $values);
switch ($relationType) {
case 'onetoone':
$results = call_user_func([$repository, "loadOne"]);
$item->$name = $results ?: new $baseEntity();
break;
case 'onetomany':
$results = call_user_func([$repository, $relation->function]);
foreach ($collection as $item) {
$item->$name = $baseEntity::entityCollection();
$item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty));
}
break;
}
}
}
}
public function replace(/*object|array*/ $entity, ? array $fieldsAndValue = null) : bool
{
return $this->save($entity, $fieldsAndValue, true);
}
public function replaceAll(/*EntityCollection|array*/ $collection) : void
public function replaceAll(/*EntityCollection|array*/ $collection) : int
{
$changed = 0;
foreach($collection as $entity) {
$this->replace($entity);
$this->replace($entity) && $changed++;
}
return $changed;
}
public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self
@ -489,10 +435,10 @@ class Repository
public function randomizeOrder() : self
{
$this->queryBuilder->orderBy(Common\Sql::function('RAND', Sql::identifier('CURDATE()+0')));
return $this;
}
public function orders(array $orderList) : self
{
foreach($orderList as $field => $direction) {
@ -678,6 +624,64 @@ class Repository
return $this;
}
public function loadCollectionRelation(EntityCollection $collection, /*array|string*/ $fields) : void
{
foreach ((array)$fields as $name) {
if (null !== ($relation = $this->entityResolver->searchFieldAnnotation($name, new Annotation\Property\Relation()))) {
$relationType = strtolower(str_replace(['-', '_', ' '], '', $relation->type));
$order = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\OrderBy());
$where = $this->entityResolver->searchFieldAnnotationList($name, new Annotation\Property\Where());
$baseEntity = $relation->entity ?? $relation->bridge ?? $this->entityResolver->properties[$name]['type'];
$baseEntityResolver = $baseEntity::resolveEntity();
$property = ($baseEntityResolver->field($relation->foreignKey, 01, false) ?: $baseEntityResolver->field($relation->foreignKey, 02))['name'];
$entityProperty = ($this->entityResolver->field($relation->key, 01, false) ?: $this->entityResolver->field($relation->key, 02))['name'];
$repository = $baseEntity::repository();
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);
}
foreach ($order as $item) {
$repository->orderBy($item->field, $item->order);
}
$field = $relation->key;
$values = [];
$key = is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey);
foreach ($collection as $item) {
$values[] = is_callable($field) ? $field($item) : $item->$entityProperty;
}
$repository->where($key, $values);
switch ($relationType) {
case 'onetoone':
$results = call_user_func([$repository, "loadOne"]);
$item->$name = $results ?: new $baseEntity();
break;
case 'onetomany':
$results = call_user_func([$repository, $relation->function]);
foreach ($collection as $item) {
$item->$name = $baseEntity::entityCollection();
$item->$name->mergeWith($results->filtersCollection(fn($e) => $e->$property === $item->$entityProperty));
}
break;
}
}
}
}
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{
if ($count) {
@ -819,13 +823,7 @@ class Repository
public function createSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) {
$this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
}
if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) {
if ( $engine = $this->entityResolver->tableAnnotation()->engine ?? $this->entityResolver->databaseAdapter()->adapter()->defaultEngine() ) {
$this->queryBuilder->engine($engine);
}
$this->queryBuilder->create($this->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
}
return $this;
@ -834,7 +832,7 @@ class Repository
public function alterSqlQuery(array $fields) : self
{
if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) {
$this->queryBuilder->create($this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
$this->queryBuilder->create($this->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
}
@ -859,7 +857,7 @@ class Repository
return $result;
}
public function instanciateEntityCollection(...$arguments) : EntityCollection
{
return $this->entityClass::entityCollection(...$arguments);
@ -870,47 +868,6 @@ class Repository
return new $this->entityClass();
}
public function escapeField(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD);
}
public function escapeFieldList(array $fieldList) : array
{
foreach($fieldList as &$list) {
$list['name'] = $this->escapeField($list['name']);
$fieldTag = array_filter($list['tags'] ?? [], fn($item) => $item['object'] instanceof Field)[0]['object'] ?? null;
if ( $fieldTag && isset($fieldTag->name) ) {
$fieldTag->name = $this->escapeField($fieldTag->name);
}
}
return $fieldList;
}
public function escapeTable(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE);
}
public function escapeDatabase(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_DATABASE);
}
public function escapedDatabase() : ? string
{
$name = $this->entityResolver->tableAnnotation()->database ?? $this->adapter->adapter()->database ?? null;
return $name ? static::escapeDatabase($name) : null;
}
public function escapeSchema(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_SCHEMA);
}
public function hasFilters() : bool
{

View File

@ -0,0 +1,51 @@
<?php
namespace Ulmus\Repository;
use Ulmus\{ Query, Adapter };
use Ulmus\Annotation\Property\Field;
trait EscapeTrait
{
public function escapeField(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_FIELD);
}
public function escapeFieldList(array $fieldList) : array
{
foreach($fieldList as &$list) {
$list['name'] = $this->escapeField($list['name']);
$fieldTag = array_filter($list['tags'] ?? [], fn($item) => $item['object'] instanceof Field)[0]['object'] ?? null;
if ( $fieldTag && isset($fieldTag->name) ) {
$fieldTag->name = $this->escapeField($fieldTag->name);
}
}
return $fieldList;
}
public function escapeTable(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_TABLE);
}
public function escapeDatabase(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_DATABASE);
}
public function escapedDatabase() : ? string
{
$name = $this->entityResolver->tableAnnotation()->database ?? $this->adapter->adapter()->database ?? null;
return $name ? static::escapeDatabase($name) : null;
}
public function escapeSchema(string $identifier) : string
{
return $this->adapter->adapter()->escapeIdentifier($identifier, Adapter\AdapterInterface::IDENTIFIER_SCHEMA);
}
}

View File

@ -5,7 +5,7 @@ namespace Ulmus\Repository;
use Ulmus\{ Repository, Query };
class MssqlRepository extends Repository {
/*
protected function finalizeQuery() : void
{
if ( null !== $offset = $this->queryBuilder->getFragment(Query\Offset::class) ) {
@ -39,5 +39,34 @@ class MssqlRepository extends Repository {
$this->queryBuilder->removeFragment($limit);
}
}
*/
protected function finalizeQuery() : void
{
if ( null !== $offset = $this->queryBuilder->getFragment(Query\MsSQL\Offset::class) ) {
# an order by is mandatory for mssql offset/limit
if ( null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class) ) {
$this->orderBy("(SELECT 0)");
}
if ( empty ($offset->offset ) ) {
if ( null !== $select = $this->queryBuilder->getFragment(Query\Select::class) ) {
$select->top = $offset->limit;
}
elseif ( null !== $delete = $this->queryBuilder->getFragment(Query\Delete::class) ) {
$delete->top = $offset->limit;
}
$this->queryBuilder->removeFragment($offset);
}
elseif ( empty($offset->limit) ) {
throw new \Exception("Your offset query fragment is missing a LIMIT value.");
}
}
}
protected function serverRequestCountRepository() : Repository
{
return new static($this->entityClass, $this->alias, $this->adapter);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Ulmus\Repository;
use Ulmus\{Common\EntityResolver, ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus};
class MysqlRepository extends Repository {
public function pragma(/*object|Stringable*/ $pragma, $argument = null, bool $callable = false) : self
{
$this->queryBuilder->pragma($pragma, $argument, $callable);
return $this;
}
public function createSqlQuery() : self
{
if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) {
if ( $engine = $this->entityResolver->tableAnnotation()->engine ?? $this->entityResolver->databaseAdapter()->adapter()->defaultEngine() ) {
$this->queryBuilder->engine($engine);
}
}
return parent::createSqlQuery();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Ulmus\Repository;
use Ulmus\{ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus};
class SqliteRepository extends Repository {
public function pragma(/*object|Stringable*/ $pragma, $argument = null, bool $callable = false) : self
{
$this->queryBuilder->pragma($pragma, $argument, $callable);
return $this;
}
protected function finalizeQuery() : void
{
}
protected function serverRequestCountRepository() : Repository
{
return new static($this->entityClass, $this->alias, $this->adapter);
}
}

View File

@ -41,4 +41,10 @@ trait SearchRequestPaginationTrait {
{
return $this->pageCount() > 1;
}
public function skipCount(bool $value) : self
{
$this->skipCount = $value;
return $this;
}
}

View File

@ -102,14 +102,18 @@ abstract class Ulmus
return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass);
}
public static function repository(...$arguments) : Repository
public static function repository(string $entityClass, ...$arguments) : Repository
{
return new static::$repositoryClass(...$arguments);
$cls = $entityClass::resolveEntity()->sqlAdapter()->adapter()->repositoryClass();
return new $cls($entityClass, ...$arguments);
}
public static function queryBuilder(...$arguments) : Query\QueryBuilderInterface
public static function queryBuilder($entityClass, ...$arguments) : Query\QueryBuilderInterface
{
return new static::$queryBuilderClass(...$arguments);
$cls = $entityClass::resolveEntity()->sqlAdapter()->adapter()->queryBuilderClass();
return new $cls(...$arguments);
}
public static function instanciateObject(string $type, array $arguments) : object