40 Commits
Author SHA1 Message Date
mcndave a18b7dba6c - WIP on package manager 2026-07-16 16:29:42 -04:00
Dave Mc Nicoll 447430b7bb - Fixed PDO return value of connect() in mssql 2026-07-14 20:12:03 +00:00
mcndave 00093b0398 - Error message now more verbose 2026-07-14 19:40:54 +00:00
mcndave 1dcf32e0da - Working on revamping Ulmus bases classes 2026-07-13 19:41:44 +00:00
mcndave 13b7cf06f1 - ArrayOf errors now catched in EntityTrait (event soon to be attached to it)
- Fixed some non-nullable defaulted to null vars in functions
- Added a new diff method for entities which skips nullable values if they are not defined.
2026-07-13 12:33:40 +00:00
mcndave 709ed63323 - Still working on SearchRequest for optimized and more advanced query manipulations
- Minor bugfixes in DatasetHandler and ObjectInstanciator
2026-07-01 13:00:01 -04:00
Dave Mc Nicoll 5d32d9f635 - Fixed MsSQL entity and searchRequest which caused buggy searchs 2026-06-19 19:44:41 +00:00
Dave Mc Nicoll 1e4a528d39 - Merged 2026-06-15 19:58:46 +00:00
Dev 2d86adc3e1 - Added some more info on a bad connection error 2026-06-09 19:27:17 +00:00
Dave Mc Nicoll 2a03789e7c - Defined nullable adapter 2026-06-01 15:11:07 +00:00
Dave Mc Nicoll 1af0063203 - WIP on php8.5 migration 2026-05-22 20:36:35 +00:00
Dave Mc Nicoll 7b35de3258 - Work done on search request mostly 2026-05-21 15:19:35 +00:00
Dave Mc Nicoll f938e18572 - Added a 'select' attribute, WIP on ArrayCollection and ArrayOf also 2026-04-24 19:26:02 +00:00
Dave Mc Nicoll b394b9348f - Specified a bit more how debug arguments are printed 2026-04-23 18:11:31 +00:00
mcndave 009e30eb7e - Added ArrayOf attributes and DatasetFill/DatasetPull events. ArrayCollection is also working with ArrayOf attributes 2026-04-13 17:00:29 +00:00
Dave Mc Nicoll fd5d31fa17 - Merged getValue override 2026-02-25 20:09:28 +00:00
Dev b0b3aca6d9 - Fixed error throwing while building relation 2026-01-16 21:28:40 +00:00
mcndave 36162c6486 -Empty dataset can not be saved 2026-01-05 12:37:55 +00:00
Dev 11e436236c - Heavy rework on search request handling of entities 2025-12-02 00:15:54 +00:00
Dev a9b9c2cce9 - Heavy rework on search request handling of entities 2025-12-02 00:15:26 +00:00
Dave Mc Nicoll cb125545c7 - WIP on search request handling of operators 2025-11-11 18:22:44 +00:00
Dave Mc Nicoll 2271b0b034 - Added a Trim PropertyValueModifier 2025-10-15 15:03:09 +00:00
Dave Mc Nicoll 2e45eba520 - Added some more debug info, fixed missing interface on Entity 2025-09-17 20:30:22 +00:00
mcndave 033a183269 - Fixed a bug with an incorrect execute 2025-07-22 13:14:41 +00:00
mcndave 0192975f10 - Removed 'change' form sql migration alter column in sqlite 2025-07-17 18:35:27 +00:00
mcndave b33c67b594 - WIP on locale formatting since strftime is deprecated
- Added Limit and Offset attributes in RelationBuilder
2025-07-14 16:54:45 +00:00
Dave Mc Nicoll 2f482324b2 - Added callable field in search, searchOne, remove, removeOne, etc... 2025-06-26 14:34:12 +00:00
Dave Mc Nicoll a5d65025f8 - Fixed a bug within mssql migration 2025-06-20 13:11:56 +00:00
mcndave b53327a6ed - Refactored PdoObject class a bit 2025-06-10 14:46:37 +00:00
Dave Mc Nicoll fb644b0b0a - Added withJoin from attributes in RelationBuilder (can't understand why it wasn't already..) 2025-06-03 20:27:15 +00:00
Dave Mc Nicoll c719e4a6d1 - WIp on Migration for mssql 2025-05-26 18:21:37 +00:00
Dave Mc Nicoll fb4985160a - WIP on Migration of MsSQL 2025-05-21 18:46:34 +00:00
Dave Mc Nicoll c54de30069 - Fixed evalClosure() missing type which converted EntityField into string 2025-05-16 12:59:31 +00:00
Dave Mc Nicoll 8fff21ecb0 - Fixed RelationBuilder problem whenever a field wasn't set 2025-05-15 18:09:52 +00:00
Dave Mc Nicoll cc6048d4ee - WIP on migration and some bug fixes linked to withJoin() select problems 2025-05-15 18:05:23 +00:00
Dave Mc Nicoll 761e0401b0 - Fixed SQLite truncate, added AdapterProxy autoloading 2025-05-12 15:11:00 +00:00
Dave Mc Nicoll 1ec582d1ac - Comparison made before transforming to string was skipping nulls and not nulls but empty 2025-04-03 19:38:48 +00:00
Dave Mc Nicoll 9556dea849 - Fixed a bug where withJoin() would mess with previous select() done on querybuilding
- Fixed a problem related with distinct in select whenever another select on fields was done
2025-03-20 17:31:19 +00:00
Dave Mc Nicoll bfddf84564 - Added partial support for Index as primary keys 2025-03-11 19:36:00 +00:00
Dave Mc Nicoll e0ec140661 - Sqlite now handles concurrency with transactions 2025-02-25 14:09:41 +00:00
94 changed files with 2141 additions and 573 deletions

No files matched your search

+3 -3
View File
@@ -10,12 +10,12 @@
} }
], ],
"require": { "require": {
"mcnd/notes": "dev-master" "mcnd/notes": "^2.1.1"
}, },
"repositories": [ "repositories": [
{ {
"type": "vcs", "type": "composer",
"url": "https://github.com/mcNdave/notes.git" "url": "https://git.mcnd.ca/api/packages/mcndave/composer"
} }
], ],
"autoload": { "autoload": {
+1 -2
View File
@@ -2,7 +2,6 @@
namespace Ulmus\Adapter; namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
interface AdapterInterface { interface AdapterInterface {
@@ -12,7 +11,7 @@ interface AdapterInterface {
public const IDENTIFIER_SCHEMA = 4; public const IDENTIFIER_SCHEMA = 4;
public const IDENTIFIER_VALUE = 5; public const IDENTIFIER_VALUE = 5;
public function connect() : object /* | PdoObject|mixed */; public function connect() : object;
public function buildDataSourceName() : string; public function buildDataSourceName() : string;
public function setup(array $configuration) : void; public function setup(array $configuration) : void;
public function writableValue(/* mixed */ $value); /*: mixed*/ public function writableValue(/* mixed */ $value); /*: mixed*/
-121
View File
@@ -1,121 +0,0 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder\Sql\MysqlQueryBuilder, Entity};
trait DefaultAdapterTrait
{
public function repositoryClass() : string
{
return Repository::class;
}
public function queryBuilderClass() : string
{
return MysqlQueryBuilder::class;
}
public function tableSyntax() : array
{
return [
'ai' => "AUTO_INCREMENT",
'pk' => "PRIMARY KEY",
'unsigned' => "UNSIGNED",
];
}
public function databaseName() : string
{
return $this->database;
}
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);
}
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 . ( isset($length) ? "($length" . ( ! empty($precision) ? ",$precision" : "" ) . ")" : "" );
}
public function whitelistAttributes(array &$parameters) : void
{
$parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES));
}
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if ($field['previous']) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']['field'], AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
return implode(" ", [
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position,
]);
}
}
+27 -6
View File
@@ -2,17 +2,22 @@
namespace Ulmus\Adapter; namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject; use Ulmus\Pdo;
use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Ulmus; use Ulmus\Ulmus;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\{Entity\InformationSchema\Table, Migration\MigrateInterface, Repository, QueryBuilder}; use Ulmus\{ConnectionAdapter,
Entity\InformationSchema\Table,
Migration\MigrateInterface,
Migration\SqlMigrationTrait,
Repository,
QueryBuilder};
class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'default', 'primary_key', 'auto_increment',
@@ -78,10 +83,10 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
} }
} }
public function connect() : PdoObject public function connect() : \PDO
{ {
try { try {
$pdo = new PdoObject($this->buildDataSourceName(), $this->username, $this->password); $pdo = new Pdo\Mssql($this->buildDataSourceName(), $this->username, $this->password);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
$pdo->setAttribute(\PDO::SQLSRV_ATTR_ENCODING, \PDO::SQLSRV_ENCODING_UTF8); $pdo->setAttribute(\PDO::SQLSRV_ATTR_ENCODING, \PDO::SQLSRV_ENCODING_UTF8);
@@ -205,7 +210,14 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
$this->traceOn = $configuration['trace_on']; $this->traceOn = $configuration['trace_on'];
} }
} }
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new MsSQLFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public static function escapeIdentifier(string $segment, int $type) : string public static function escapeIdentifier(string $segment, int $type) : string
{ {
switch($type) { switch($type) {
@@ -235,4 +247,13 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
{ {
return QueryBuilder\Sql\MssqlQueryBuilder::class; return QueryBuilder\Sql\MssqlQueryBuilder::class;
} }
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
return \Ulmus\Entity\Mssql\Table::repository(Repository::DEFAULT_ALIAS, $adapter)
->select(\Ulmus\Common\Sql::raw('this.*'))
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->or($this->escapeIdentifier('table_catalog', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
}
} }
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Migration\FieldDefinition;
class MsSQLFieldMapper extends SqlFieldMapper
{
public function map() : void
{
parent::map();
if (in_array($this->type, [ 'CHAR', 'VARCHAR', 'TEXT', ])) {
$this->type = "N" . $this->type;
};
}
/* @TODO !
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);
}
}
* */
}
+27 -4
View File
@@ -6,14 +6,15 @@ use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\Table; use Ulmus\Entity\Mysql\Table;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\Migration\MigrateInterface; use Ulmus\Migration\MigrateInterface;
use Ulmus\Migration\SqlMigrationTrait;
use Ulmus\QueryBuilder\Sql; use Ulmus\QueryBuilder\Sql;
use Ulmus\Common\PdoObject; use Ulmus\Pdo;
use Ulmus\Exception\AdapterConfigurationException; use Ulmus\Exception\AdapterConfigurationException;
use Ulmus\Repository; use Ulmus\Repository;
class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'update', 'default', 'primary_key', 'auto_increment', 'update',
@@ -70,10 +71,10 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
} }
} }
public function connect() : PdoObject public function connect() : \PDO
{ {
try { try {
$pdo = new PdoObject($this->buildDataSourceName(), $this->username, $this->password); $pdo = new Pdo\Mysql($this->buildDataSourceName(), $this->username, $this->password);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, false); $pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, false);
@@ -180,4 +181,26 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName) ->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); ->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
} }
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if ($field['action'] === 'add') {
if (! empty($field['previous'])) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
}
return implode(" ", array_filter([
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position ?? null,
]));
}
} }
-4
View File
@@ -6,10 +6,6 @@ use Ulmus\Migration\FieldDefinition;
class MySQLFieldMapper extends SqlFieldMapper class MySQLFieldMapper extends SqlFieldMapper
{ {
public readonly string $type;
public readonly string $length;
public function map() : void public function map() : void
{ {
$type = $this->field->type; $type = $this->field->type;
+48 -46
View File
@@ -3,14 +3,14 @@
namespace Ulmus\Adapter; namespace Ulmus\Adapter;
use Collator; use Collator;
use Ulmus\Common\PdoObject; use Ulmus\Pdo;
use Ulmus\ConnectionAdapter; use Ulmus\ConnectionAdapter;
use Ulmus\Entity; use Ulmus\Entity;
use Ulmus\Migration\FieldDefinition; use Ulmus\Migration\FieldDefinition;
use Ulmus\{Migration\MigrateInterface, Repository, QueryBuilder}; use Ulmus\{Migration\MigrateInterface, Migration\SqlMigrationTrait, Repository, QueryBuilder};
class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface { class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
use SqlAdapterTrait; use SqlAdapterTrait, SqlMigrationTrait;
const ALLOWED_ATTRIBUTES = [ const ALLOWED_ATTRIBUTES = [
'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim', 'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim',
@@ -24,15 +24,15 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
public null|array $pragmaClose = null, public null|array $pragmaClose = null,
) { } ) { }
public function connect() : PdoObject public function connect() : \Pdo
{ {
try { try {
$pdo = new PdoObject\SqlitePdoObject($this->buildDataSourceName(), null, null); $pdo = new Pdo\Sqlite($this->buildDataSourceName(), null, null);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
$pdo->onClose = function(PdoObject $obj) { $pdo->onClose = function(\PDO $obj) {
static::registerPragma($obj, $this->pragmaClose); static::registerPragma($obj, $this->pragmaClose);
}; };
@@ -102,7 +102,8 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
if ( is_a($type, Entity\Field\Date::class, true) || is_a($type, Entity\Field\Time::class, true) || is_a($type, \DateTime::class, true) ) { 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"; $type = "TEXT";
$length = strlen((string) $type);
#$length = strlen((string) $type);
} }
else { else {
switch($type) { switch($type) {
@@ -134,10 +135,6 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
} }
} }
if (in_array($type, [ 'JSON', 'TEXT', 'BLOB', 'GEOMETRY' ])) {
unset($field->default);
}
return $typeOnly ? $type : $type . ( $length ? "($length" . ")" : "" ); return $typeOnly ? $type : $type . ( $length ? "($length" . ")" : "" );
} }
@@ -160,28 +157,28 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
return QueryBuilder\Sql\SqliteQueryBuilder::class; return QueryBuilder\Sql\SqliteQueryBuilder::class;
} }
public function exportFunctions(PdoObject $pdo) : void public function exportFunctions(\PDO $pdo) : void
{ {
$pdo->sqliteCreateFunction('if', fn($comparison, $yes, $no) => $comparison ? $yes : $no, 3); $pdo->createFunction('if', fn($comparison, $yes, $no) => $comparison ? $yes : $no, 3);
$pdo->sqliteCreateFunction('length', fn($string) => strlen($string), 1); $pdo->createFunction('length', fn($string) => strlen($string), 1);
$pdo->sqliteCreateFunction('lcase', fn($string) => strtolower($string), 1); $pdo->createFunction('lcase', fn($string) => strtolower($string), 1);
$pdo->sqliteCreateFunction('ucase', fn($string) => strtoupper($string), 1); $pdo->createFunction('ucase', fn($string) => strtoupper($string), 1);
$pdo->sqliteCreateFunction('md5', fn($string) => md5($string), 1); $pdo->createFunction('md5', fn($string) => md5($string), 1);
$pdo->sqliteCreateFunction('sha1', fn($string) => sha1($string), 1); $pdo->createFunction('sha1', fn($string) => sha1($string), 1);
$pdo->sqliteCreateFunction('sha256', fn($string) => hash('sha256', $string), 1); $pdo->createFunction('sha256', fn($string) => hash('sha256', $string), 1);
$pdo->sqliteCreateFunction('sha512', fn($string) => hash('sha512', $string), 1); $pdo->createFunction('sha512', fn($string) => hash('sha512', $string), 1);
$pdo->sqliteCreateFunction('whirlpool', fn($string) => hash('whirlpool', $string), 1); $pdo->createFunction('whirlpool', fn($string) => hash('whirlpool', $string), 1);
$pdo->sqliteCreateFunction('murmur3a', fn($string) => hash('murmur3a', $string), 1); $pdo->createFunction('murmur3a', fn($string) => hash('murmur3a', $string), 1);
$pdo->sqliteCreateFunction('murmur3c', fn($string) => hash('murmur3c', $string), 1); $pdo->createFunction('murmur3c', fn($string) => hash('murmur3c', $string), 1);
$pdo->sqliteCreateFunction('murmur3f', fn($string) => hash('murmur3f', $string), 1); $pdo->createFunction('murmur3f', fn($string) => hash('murmur3f', $string), 1);
$pdo->sqliteCreateFunction('left', fn($string, $length) => substr($string, 0, $length), 2); $pdo->createFunction('left', fn($string, $length) => substr($string, 0, $length), 2);
$pdo->sqliteCreateFunction('right', fn($string, $length) => substr($string, -$length), 2); $pdo->createFunction('right', fn($string, $length) => substr($string, -$length), 2);
$pdo->sqliteCreateFunction('strcmp', fn($s1, $s2) => strcmp($s1, $s2), 2); $pdo->createFunction('strcmp', fn($s1, $s2) => strcmp($s1, $s2), 2);
$pdo->sqliteCreateFunction('lpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_LEFT), 3); $pdo->createFunction('lpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_LEFT), 3);
$pdo->sqliteCreateFunction('rpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_RIGHT), 3); $pdo->createFunction('rpad', fn($string, $length, $pad) => str_pad($string, $length, $pad, STR_PAD_RIGHT), 3);
$pdo->sqliteCreateFunction('concat', fn(...$args) => implode('', $args), -1); $pdo->createFunction('concat', fn(...$args) => implode('', $args), -1);
$pdo->sqliteCreateFunction('concat_ws', fn($separator, ...$args) => implode($separator, $args), -1); $pdo->createFunction('concat_ws', fn($separator, ...$args) => implode($separator, $args), -1);
$pdo->sqliteCreateFunction('find_in_set', function($string, $string_list) { $pdo->createFunction('find_in_set', function($string, $string_list) {
if ( $string === null || $string_list === null ) { if ( $string === null || $string_list === null ) {
return null; return null;
} }
@@ -192,13 +189,13 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
return (int) in_array($string, explode(',', $string_list)); return (int) in_array($string, explode(',', $string_list));
}, 2); }, 2);
$pdo->sqliteCreateFunction('day', fn($date) => ( new \DateTime($date) )->format('j'), 1); $pdo->createFunction('day', fn($date) => ( new \DateTime($date) )->format('j'), 1);
$pdo->sqliteCreateFunction('month', fn($date) => ( new \DateTime($date) )->format('n'), 1); $pdo->createFunction('month', fn($date) => ( new \DateTime($date) )->format('n'), 1);
$pdo->sqliteCreateFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1); $pdo->createFunction('year', fn($date) => ( new \DateTime($date) )->format('Y'), 1);
$pdo->createFunction('str_to_date', fn($value, $format) => ($f = \DateTime::createFromFormat($format, $value)) ? $f->format('Y-m-d H:i:s') : false);
} }
public function exportCollations(\PDO $pdo) : void
public function exportCollations(PdoObject $pdo) : void
{ {
if ( class_exists('Locale') ) { if ( class_exists('Locale') ) {
@@ -207,28 +204,28 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
$collator->setStrength(Collator::TERTIARY); $collator->setStrength(Collator::TERTIARY);
$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); $collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
$pdo->sqliteCreateCollation('locale_cmp', fn($e1, $e2) => $collator->compare($e1, $e2)); $pdo->createCollation('locale_cmp', fn($e1, $e2) => $collator->compare($e1, $e2));
$pdo->sqliteCreateCollation('locale_cmp_desc', fn($e1, $e2) => $collator->compare($e2, $e1)); $pdo->createCollation('locale_cmp_desc', fn($e1, $e2) => $collator->compare($e2, $e1));
$collatorCi = new Collator(\Locale::getDefault()); $collatorCi = new Collator(\Locale::getDefault());
$collatorCi->setStrength(Collator::SECONDARY); $collatorCi->setStrength(Collator::SECONDARY);
$collatorCi->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON); $collatorCi->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
$pdo->sqliteCreateCollation('locale_cmp_ci', fn($e1, $e2) => $collatorCi->compare($e1, $e2)); $pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $collatorCi->compare($e1, $e2));
$pdo->sqliteCreateCollation('locale_cmp_ci_desc', fn($e1, $e2) => $collatorCi->compare($e2, $e1)); $pdo->createCollation('locale_cmp_ci_desc', fn($e1, $e2) => $collatorCi->compare($e2, $e1));
} }
else { else {
$comp = fn(string $e1, string $e2) => \iconv('UTF-8', 'ASCII//TRANSLIT', $e1) <=> \iconv('UTF-8', 'ASCII//TRANSLIT', $e2); $comp = fn(string $e1, string $e2) => \iconv('UTF-8', 'ASCII//TRANSLIT', $e1) <=> \iconv('UTF-8', 'ASCII//TRANSLIT', $e2);
$pdo->sqliteCreateCollation('locale_cmp', fn($e1, $e2) => $comp); $pdo->createCollation('locale_cmp', fn($e1, $e2) => $comp);
$pdo->sqliteCreateCollation('locale_cmp_desc', fn($e2, $e1) => $comp); $pdo->createCollation('locale_cmp_desc', fn($e2, $e1) => $comp);
$compCi = fn(string $e1, string $e2) => strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e1)) <=> strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e2)); $compCi = fn(string $e1, string $e2) => strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e1)) <=> strtoupper(\iconv('UTF-8', 'ASCII//TRANSLIT', $e2));
$pdo->sqliteCreateCollation('locale_cmp_ci', fn($e1, $e2) => $compCi); $pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $compCi);
$pdo->sqliteCreateCollation('locale_cmp_ci_desc', fn($e2, $e1) => $compCi); $pdo->createCollation('locale_cmp_ci_desc', fn($e2, $e1) => $compCi);
} }
} }
public static function registerPragma(PdoObject $pdo, array $pragmaList) : void public static function registerPragma(\PDO $pdo, array $pragmaList) : void
{ {
$builder = new QueryBuilder\Sql\SqliteQueryBuilder(); $builder = new QueryBuilder\Sql\SqliteQueryBuilder();
@@ -256,6 +253,11 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
]); ]);
} }
public function supportAlterColumnAction(string $action)
{
return in_array(strtolower($action), [ 'add' ]);
}
public function splitAlterQuery() : bool public function splitAlterQuery() : bool
{ {
return true; return true;
-38
View File
@@ -36,44 +36,11 @@ trait SqlAdapterTrait
return $this->database; return $this->database;
} }
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);
}
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new SqlFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public function whitelistAttributes(array &$parameters) : void public function whitelistAttributes(array &$parameters) : void
{ {
$parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES)); $parameters = array_intersect_key($parameters, array_flip(static::ALLOWED_ATTRIBUTES));
} }
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
if (! empty($field['previous']) ) {
$position = sprintf('AFTER %s', $this->escapeIdentifier($field['previous']->field, AdapterInterface::IDENTIFIER_FIELD));
}
else {
$position = "FIRST";
}
return implode(" ", [
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
$position,
]);
}
public function writableValue(mixed $value) : mixed public function writableValue(mixed $value) : mixed
{ {
switch (true) { switch (true) {
@@ -92,9 +59,4 @@ trait SqlAdapterTrait
return $value; return $value;
} }
public function splitAlterQuery() : bool
{
return false;
}
} }
+4 -3
View File
@@ -7,9 +7,9 @@ use Ulmus\Entity;
class SqlFieldMapper class SqlFieldMapper
{ {
public readonly string $type; public string $type;
public readonly string $length; public string $length;
public function __construct( public function __construct(
public FieldDefinition $field, public FieldDefinition $field,
@@ -47,7 +47,8 @@ class SqlFieldMapper
break; break;
case "array": case "array":
$this->type = "JSON"; # Was set at JSON, but for now LONGTEXT is an alias of it (with additional constraints)
$this->type = "LONGTEXT";
break; break;
case "string": case "string":
+6
View File
@@ -10,6 +10,12 @@ class Attribute
public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
{ {
if ( is_array($field) ) { if ( is_array($field) ) {
if (count($field) < 2) {
throw new \RuntimeException(
sprintf("Array field must be formed of at least two things, a class name, and a property. (received %s)", json_encode($field))
);
}
$class = array_shift($field); $class = array_shift($field);
$field[1] ??= $alias; $field[1] ??= $alias;
+1 -1
View File
@@ -11,4 +11,4 @@ class Index {
public IndexTypeEnum $type = IndexTypeEnum::Unique, public IndexTypeEnum $type = IndexTypeEnum::Unique,
public null|string $name = null, public null|string $name = null,
) {} ) {}
} }
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute\Obj;
#[\Attribute(\Attribute::TARGET_CLASS)]
class JsonSerialize {
public function __construct(
public bool $includeRelations = true,
) {}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Ulmus\Attribute\Property;
use Notes\Common\ReflectedProperty;
use Ulmus\Entity\EntityInterface;
use Ulmus\Entity\JsonUnserializable;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class ArrayOf implements \Ulmus\Entity\EntityValueModifier
{
public function __construct(
public readonly string $type,
) {}
public function push(mixed $value, ReflectedProperty $property): mixed
{
$return = [];
foreach($value as $index => $item) {
if (! is_object($item)) {
if (enum_exists($this->type)) {
if ($this->type::tryFrom($item) === null) {
throw new \InvalidArgumentException(
sprintf("Given item '%s' is not a valid enum for type %s ; try one of theses instead : %s", $item, $this->type, implode("', '", array_map(fn($e) => $e->value, $this->type::cases())))
);
} else {
$obj = $this->type::from($item);
}
}
elseif (class_exists($this->type)) {
if (is_subclass_of($this->type, JsonUnserializable::class)) {
$obj = (new \ReflectionClass($this->type))->newInstanceWithoutConstructor();
$obj->jsonUnserialize($item);
}
else {
$obj = new $this->type($item);
}
}
else {
throw new \InvalidArgumentException("Given type for ArrayOf is incompatible with it's use. An object should be provided");
}
$return[$index] = $obj;
}
}
if (! $property->type->builtIn) {
$cls = $property->type->type;
return new $cls($return);
}
return $return;
}
public function pull(mixed $value, ReflectedProperty $property) : mixed
{
$return = [];
foreach($value as $index => $item) {
if (is_object($item)) {
if (enum_exists($this->type)) {
$value = $item->value;
} elseif (class_exists($this->type)) {
$value = (string) $value;
}
$return[$index] = $value;
}
}
return $return;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute\Property\Field;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Select {
public function __construct(
public string $sql,
) {}
}
+1 -2
View File
@@ -6,6 +6,5 @@ namespace Ulmus\Attribute\Property;
class Filter { class Filter {
public function __construct( public function __construct(
public string $method = "" public string $method = ""
) ) {}
{}
} }
+32
View File
@@ -3,6 +3,7 @@
namespace Ulmus\Attribute\Property; namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
use Ulmus\Repository;
#[\Attribute] #[\Attribute]
class Join implements ResettablePropertyInterface { class Join implements ResettablePropertyInterface {
@@ -16,4 +17,35 @@ class Join implements ResettablePropertyInterface {
$this->key = Attribute::handleArrayField($this->key); $this->key = Attribute::handleArrayField($this->key);
$this->foreignKey = Attribute::handleArrayField($this->foreignKey); $this->foreignKey = Attribute::handleArrayField($this->foreignKey);
} }
## AWAITING THE Closures in constant expression from PHP 8.5 !
public function foreignKey(? Repository $repository = null) : mixed
{
if (is_string($this->foreignKey)) {
if (str_starts_with($this->foreignKey, 'fn(') && str_contains($this->foreignKey, '=>')) {
$closure = eval("return {$this->foreignKey};");
return $repository ? $closure($repository) : $closure;
}
return $this->entity::field($this->foreignKey, $this->alias);
}
return $this->foreignKey;
}
public function isOneToOne() : bool
{
return true;
}
public function isOneToMany() : bool
{
return false;
}
public function isManyToMany() : bool
{
return false;
}
} }
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class JsonSerialize {
public function __construct(
public bool $ignoreField = false,
) {}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Limit {
public function __construct(
public int $value,
) {}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Offset {
public function __construct(
public int $value,
) {}
}
+18
View File
@@ -3,6 +3,7 @@
namespace Ulmus\Attribute\Property; namespace Ulmus\Attribute\Property;
use Ulmus\Attribute\Attribute; use Ulmus\Attribute\Attribute;
use Ulmus\Repository;
#[\Attribute(\Attribute::TARGET_PROPERTY)] #[\Attribute(\Attribute::TARGET_PROPERTY)]
class Relation implements ResettablePropertyInterface { class Relation implements ResettablePropertyInterface {
@@ -21,6 +22,7 @@ class Relation implements ResettablePropertyInterface {
public null|string $entity = null, public null|string $entity = null,
public null|string $join = null, public null|string $join = null,
public null|string $function = null, public null|string $function = null,
public null|string $alias = null,
) { ) {
$this->key = Attribute::handleArrayField($this->key); $this->key = Attribute::handleArrayField($this->key);
$this->foreignKey = Attribute::handleArrayField($this->foreignKey); $this->foreignKey = Attribute::handleArrayField($this->foreignKey);
@@ -83,4 +85,20 @@ class Relation implements ResettablePropertyInterface {
{ {
return (bool) $this->bridge; return (bool) $this->bridge;
} }
## AWAITING THE Closures in constant expression from PHP 8.5 !
public function foreignKey(? Repository $repository = null) : mixed
{
if (is_string($this->foreignKey)) {
if (str_starts_with($this->foreignKey, 'fn(') && str_contains($this->foreignKey, '=>')) {
$closure = eval("return {$this->foreignKey};");
return $repository ? $closure($repository) : $closure;
}
return $this->entity::field($this->foreignKey, $this->alias);
}
return $this->foreignKey;
}
} }
+1
View File
@@ -10,6 +10,7 @@ class Virtual extends Field implements ResettablePropertyInterface {
public function __construct( public function __construct(
public null|string|array $method = null, public null|string|array $method = null,
public ? \Closure $closure = null, public ? \Closure $closure = null,
public null|string $name = null,
) { ) {
$this->method ??= [ static::class, 'noop' ]; $this->method ??= [ static::class, 'noop' ];
} }
+13 -7
View File
@@ -7,6 +7,7 @@ use Ulmus\Query;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class Where { class Where {
public function __construct( public function __construct(
public string|\Stringable|array $field, public string|\Stringable|array $field,
public mixed $value = null, public mixed $value = null,
@@ -21,18 +22,23 @@ class Where {
public function getValue(/* null|EntityInterface */ $entity = null) : mixed public function getValue(/* null|EntityInterface */ $entity = null) : mixed
{ {
if ($this->generateValue) { if ($entity) {
if ($entity) { if ($this->generateValue) {
return call_user_func_array($this->generateValue, [ $entity ]); return call_user_func_array($this->generateValue, [ $entity ]);
} }
else { elseif ($this->fieldValue) {
throw new \Exception(sprintf("Field value, from %s, could not be included in query since the entity is already loaded; it is meant to be used with a OneToOne relation loaded within a join.", (string) $this->fieldValue));
}
}
else {
if ($this->fieldValue) {
return $this->fieldValue;
}
elseif ($this->generateValue) {
throw new \Exception(sprintf("Could not generate value from non-instanciated entity for field %s.", (string) $this->field)); throw new \Exception(sprintf("Could not generate value from non-instanciated entity for field %s.", (string) $this->field));
} }
} }
elseif ($this->fieldValue && $entity) {
throw new \Exception(sprintf("Field value, from %s, could not be included in query since the entity is already loaded; it is meant to be used with a OneToOne relation loaded within a join.", (string) $this->fieldValue));
}
return $this->fieldValue ?? $this->value; return $this->value;
} }
} }
+1 -1
View File
@@ -5,6 +5,6 @@ namespace Ulmus\Attribute\Property;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class WithJoin { class WithJoin {
public function __construct( public function __construct(
array $joins = [] public string|array $joins = []
) {} ) {}
} }
@@ -0,0 +1,10 @@
<?php
namespace Ulmus\Attribute;
use Ulmus\Entity\EntityInterface;
interface RegisterEntityEventInterface
{
public function attach(EntityInterface $entity, string $field);
}
+20 -17
View File
@@ -10,29 +10,31 @@ use Ulmus\Ulmus,
class EntityField implements WhereRawParameter class EntityField implements WhereRawParameter
{ {
public string $name; public function __construct(
public string $name,
public string $entityClass; public string $entityClass,
public ? string $alias,
public ? string $alias; protected EntityResolver $entityResolver,
protected bool $select = false
protected EntityResolver $entityResolver; ) {}
public function __construct(string $entityClass, string $name, ? string $alias, EntityResolver $resolver)
{
$this->entityClass = $entityClass;
$this->name = $name;
$this->alias = $alias;
$this->entityResolver = $resolver;
}
public function name($useAlias = true) : string public function name($useAlias = true) : string
{ {
$name = $this->entityResolver->searchFieldAnnotation($this->name, [ 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); $name = $this->entityResolver->databaseAdapter()->adapter()->escapeIdentifier($name, AdapterInterface::IDENTIFIER_FIELD);
return $useAlias && $this->alias ? "{$this->alias}.$name" : $name; $fullname = $useAlias && $this->alias ? "{$this->alias}.$name" : $name;
if ($this->select) {
$select = $this->entityResolver->searchFieldAnnotation($this->name, [Field\Select::class]);
if ($select) {
return "(" . str_replace(['%NAME%'], [$fullname], $select->sql) . ") AS code";
}
}
return $fullname;
} }
public static function isScalarType($type) : bool public static function isScalarType($type) : bool
@@ -43,6 +45,7 @@ class EntityField implements WhereRawParameter
case 'string': case 'string':
case 'float': case 'float':
case 'double': case 'double':
case 'mixed':
return true; return true;
} }
+20 -5
View File
@@ -5,6 +5,7 @@ namespace Ulmus\Common;
use Notes\Common\ReflectedClass; use Notes\Common\ReflectedClass;
use Notes\Common\ReflectedProperty; use Notes\Common\ReflectedProperty;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Ulmus\Attribute\Obj\Index;
use Ulmus\Ulmus, use Ulmus\Ulmus,
Ulmus\Attribute\Obj\Table, Ulmus\Attribute\Obj\Table,
Ulmus\Attribute\Obj\AdapterAttributeInterface, Ulmus\Attribute\Obj\AdapterAttributeInterface,
@@ -94,7 +95,7 @@ class EntityResolver {
return $fieldList; return $fieldList;
} }
public function relation(string $name) : array public function relation(string $name, bool $throwOnError = true) : ? array
{ {
$property = $this->reflectedClass->getProperties(true)[$name] ?? false; $property = $this->reflectedClass->getProperties(true)[$name] ?? false;
@@ -110,10 +111,24 @@ class EntityResolver {
return []; return [];
} }
catch(\Throwable $e) { catch(\Throwable $e) {
throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}"); $throwOnError && throw new \InvalidArgumentException("Can't find entity relation's column named `$name` from entity {$this->entityClass}");
} }
return null;
} }
public function getPropertyHavingAttribute(array|object|string $type) : array
{
return array_filter(array_map(
fn($e) => $e->getAttributes(is_object($type) ? $type::class : $type), $this->reflectedClass->getProperties(true)
));
}
public function getPropertyEntityType(string $name) : false|string
{
return $this->reflectedClass->getProperties(true)[$name]->getTypes()[0]->type ?? false;
}
public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object public function searchFieldAnnotation(string $field, array|object|string $annotationType, bool $caseSensitive = true) : ? object
{ {
return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null; return $this->searchFieldAnnotationList($field, $annotationType, $caseSensitive)[0] ?? null;
@@ -214,9 +229,9 @@ class EntityResolver {
return null; return null;
} }
public function getCompoundKeyFields() : ? array public function getCompoundKeyFields() : ? Index
{ {
return null; return $this->getAttributeImplementing(Index::class);
} }
public function getUniqueFields() : ? array public function getUniqueFields() : ? array
+19 -27
View File
@@ -19,7 +19,7 @@ class PdoObject extends PDO {
public function select(string $sql, array $parameters = []): PDOStatement public function select(string $sql, array $parameters = []): PDOStatement
{ {
static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]); static::$dump && call_user_func_array(static::$dump, [ $this->fillDebugSqlParameters($sql, $parameters) ]);
try { try {
if (false !== ( $statement = $this->prepare($sql) )) { if (false !== ( $statement = $this->prepare($sql) )) {
@@ -43,7 +43,7 @@ class PdoObject extends PDO {
public function runQuery(string $sql, array $parameters = []): ? static public function runQuery(string $sql, array $parameters = []): ? static
{ {
static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]); static::$dump && call_user_func_array(static::$dump, [ $this->fillDebugSqlParameters($sql, $parameters) ]);
try { try {
if (false !== ( $statement = $this->prepare($sql) )) { if (false !== ( $statement = $this->prepare($sql) )) {
@@ -51,12 +51,12 @@ class PdoObject extends PDO {
} }
} }
catch(\PDOException $pdo) { catch(\PDOException $pdo) {
if ( substr($pdo->getMessage(), 0, 30) !== 'There is no active transaction' ) { if (!str_starts_with($pdo->getMessage(), 'There is no active transaction')) {
throw $pdo; throw $pdo;
} }
} }
catch (\Throwable $e) { catch (\Throwable $e) {
throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters), (int) $e->getCode(), $e);
} }
return null; return null;
@@ -83,7 +83,7 @@ class PdoObject extends PDO {
$this->lastInsertId = null; $this->lastInsertId = null;
try { try {
if ( ! $this->inTransaction() ) { if (! $this->inTransaction()) {
$this->beginTransaction(); $this->beginTransaction();
} }
@@ -110,11 +110,6 @@ class PdoObject extends PDO {
throw $e; throw $e;
} }
catch (\Throwable $e) {
throw $e;
}
return null;
} }
protected function bindVariables(PDOStatement $statement, array &$parameters) : void protected function bindVariables(PDOStatement $statement, array &$parameters) : void
@@ -125,27 +120,24 @@ class PdoObject extends PDO {
} }
else { else {
foreach ($parameters as $key => $value) { foreach ($parameters as $key => $value) {
switch (strtolower(gettype($value))) { $type = match (strtolower(gettype($value))) {
#$type = Pdo::PARAM_BOOL; "boolean", "integer" => Pdo::PARAM_INT,
#break; "null" => Pdo::PARAM_NULL,
default => Pdo::PARAM_STR,
case "boolean": };
case "integer":
$type = Pdo::PARAM_INT;
break;
case "null":
$type = Pdo::PARAM_NULL;
break;
case "string":
default:
$type = Pdo::PARAM_STR;
}
$statement->bindValue($key, $value, $type); $statement->bindValue($key, $value, $type);
} }
} }
} }
} }
protected function fillDebugSqlParameters(string $sql, array $parameters) : string
{
$parameters = array_reverse($parameters);
return str_replace(array_keys($parameters), array_map(fn($e) => is_numeric($e) ? $e : (
is_bool($e) ? (int) $e : $this->quote($e)
), array_values($parameters)), $sql);
}
} }
+7 -4
View File
@@ -12,7 +12,7 @@ class SqlPdoObject extends \Ulmus\Common\PdoObject
return $this->openTransaction(); return $this->openTransaction();
} }
return $this->exec('SAVEPOINT transaction_'.$this->openedTransaction) !== false; return $this->exec("SAVEPOINT transaction_{$this->openedTransaction}") !== false;
} }
public function commit() : bool public function commit() : bool
@@ -21,16 +21,19 @@ class SqlPdoObject extends \Ulmus\Common\PdoObject
return parent::commit(); return parent::commit();
} }
return false; return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
} }
public function rollback() : bool public function rollback() : bool
{ {
if (0 !== $this->openedTransaction) { if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false; return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
} }
elseif ($this->openedTransaction-- === 1) {
return parent::rollback();
}
return parent::rollback(); return false;
} }
protected function openTransaction() : bool protected function openTransaction() : bool
+13
View File
@@ -17,6 +17,19 @@ class SqlitePdoObject extends SqlPdoObject
return $this->exec("COMMIT") !== false; return $this->exec("COMMIT") !== false;
} }
return $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
}
public function rollback() : bool
{
if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
}
elseif ($this->openedTransaction-- === 1) {
# We must do it manually since opening a transaction manually stucks PDO into thinking we've got no transaction opened
return $this->exec('ROLLBACK') !== false;
}
return false; return false;
} }
+1 -1
View File
@@ -84,7 +84,7 @@ abstract class Sql {
return $value; return $value;
} }
public static function collate(string $name) : string public static function collate(string $name) : object
{ {
if ( ! preg_match('/^[a-z0-9$_]+$/i',$name) ) { if ( ! preg_match('/^[a-z0-9$_]+$/i',$name) ) {
throw new \InvalidArgumentException(sprintf("Given identifier '%s' should contains supported characters in function name (a-Z, 0-9, $ and _)", $name)); throw new \InvalidArgumentException(sprintf("Given identifier '%s' should contains supported characters in function name (a-Z, 0-9, $ and _)", $name));
+6 -4
View File
@@ -5,13 +5,11 @@ namespace Ulmus;
use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\CacheInterface;
use Ulmus\Adapter\AdapterInterface; use Ulmus\Adapter\AdapterInterface;
use Ulmus\Common\PdoObject;
class ConnectionAdapter class ConnectionAdapter
{ {
protected AdapterInterface $adapter; protected AdapterInterface $adapter;
protected PdoObject $pdo; protected \PDO $pdo;
public function __construct( public function __construct(
public string $name = "default", public string $name = "default",
@@ -27,6 +25,10 @@ class ConnectionAdapter
$connection = $this->configuration['connections'][$this->name] ?? []; $connection = $this->configuration['connections'][$this->name] ?? [];
if ( false !== ( $adapterName = $connection['adapter'] ?? false ) ) { if ( false !== ( $adapterName = $connection['adapter'] ?? false ) ) {
if (is_array($adapterName)) {
throw new \InvalidArgumentException(sprintf("Given adapter (%s) name is an array, not a string.. received : %s.", $this->name, json_encode($adapterName)));
}
$this->adapter = $this->instanciateAdapter($adapterName); $this->adapter = $this->instanciateAdapter($adapterName);
} }
else { else {
@@ -59,7 +61,7 @@ class ConnectionAdapter
return $this->adapter; return $this->adapter;
} }
public function pdo() : PdoObject public function pdo() : \PDO
{ {
return $this->pdo ?? $this->connect()->pdo; return $this->pdo ?? $this->connect()->pdo;
} }
+16 -1
View File
@@ -2,12 +2,27 @@
namespace Ulmus\Container; namespace Ulmus\Container;
use Ulmus\ConnectionAdapter;
class AdapterProxy class AdapterProxy
{ {
public array $connections = []; public array $connections = [];
public function __construct(...$arguments) public function __construct(...$arguments)
{ {
$this->connections = $arguments; $this->push(array_filter($arguments));
}
public function push(array|ConnectionAdapter $connection) : void
{
foreach((array) $connection as $adapter) {
foreach($this->connections as $existing) {
if ($adapter === $existing) {
continue 2;
}
}
$this->connections[] = $adapter;
}
} }
} }
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Ulmus\Entity;
use Ulmus\EntityCollection;
class ArrayCollection extends EntityCollection implements JsonUnserializable
{
public function jsonUnserialize(array $json): void
{
$this->fromArray($json);
}
public function jsonSerialize(): mixed
{
return $this->getArrayCopy();
}
public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self
{
foreach($datasets as $dataset) {
\ArrayObject::append($dataset);
}
return $this;
}
}
+18 -11
View File
@@ -17,7 +17,7 @@ class DatasetHandler
protected bool $entityStrictFieldsDeclaration = false, protected bool $entityStrictFieldsDeclaration = false,
) {} ) {}
public function pull(object $entity) : Generator public function pull(object $entity, bool $nullable = true) : Generator
{ {
foreach($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) { foreach($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true) as $key => $field) {
$attribute = $this->entityResolver->searchFieldAnnotation($key,[ Field::class ]); $attribute = $this->entityResolver->searchFieldAnnotation($key,[ Field::class ]);
@@ -25,7 +25,7 @@ class DatasetHandler
if ( $entity->__isset($key) ) { if ( $entity->__isset($key) ) {
yield $attribute->name ?? $key => $entity->$key; yield $attribute->name ?? $key => $entity->$key;
} }
elseif ( $field->allowsNull() ) { elseif ( $nullable && $field->allowsNull() ) {
yield $attribute->name ?? $key => null; yield $attribute->name ?? $key => null;
} }
} }
@@ -56,7 +56,10 @@ class DatasetHandler
} }
elseif ( $field->expectType('array') ) { elseif ( $field->expectType('array') ) {
if ( is_string($value)) { if ( is_string($value)) {
if (substr($value, 0, 1) === "a") { if (empty($value)) {
yield $field->name => [];
}
elseif (substr($value, 0, 1) === "a") {
yield $field->name => unserialize($value); yield $field->name => unserialize($value);
} }
@@ -75,7 +78,6 @@ class DatasetHandler
} }
} }
elseif ( EntityField::isScalarType($type->type) ) { elseif ( EntityField::isScalarType($type->type) ) {
if ( $type->type === 'string' ) { if ( $type->type === 'string' ) {
$attribute = $this->entityResolver->searchFieldAnnotation($field->name, [ Field::class ] ); $attribute = $this->entityResolver->searchFieldAnnotation($field->name, [ Field::class ] );
@@ -99,8 +101,10 @@ class DatasetHandler
try { try {
yield $field->name => Ulmus::instanciateObject($type->type, [ $value ]); yield $field->name => Ulmus::instanciateObject($type->type, [ $value ]);
} }
catch(\Error $e) { catch(\Throwable $e) {
throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field->name)); $type = $e::class;
throw new $type(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field->name), 101, $e);
} }
} }
} }
@@ -108,15 +112,17 @@ class DatasetHandler
return $unmatched; return $unmatched;
} }
public function pullRelation(object $entity) : Generator public function pullRelation(object $entity, bool $onlyPreviouslyLoaded = true) : Generator
{ {
foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field){ foreach($this->entityResolver->reflectedClass->getProperties(true) as $name => $field) {
$relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] ); $relation = $this->entityResolver->searchFieldAnnotation($name, [ Relation::class ] );
if ($relation) { if ($relation) {
$ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] ); $ignore = $this->entityResolver->searchFieldAnnotation($name, [ Relation\Ignore::class ] );
if ($ignore && $ignore->ignoreExport) { $load = $onlyPreviouslyLoaded ? (new \ReflectionProperty($entity::class, $name))->isInitialized($entity) : true;
if ( ! $load || ( $ignore && $ignore->ignoreExport ) ) {
if ( $relation->isOneToOne() ) { if ( $relation->isOneToOne() ) {
# @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE ! # @TODO TO INCLUDED INTO getTypes() RETURNED CLASS WHEN DONE !
yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor(); yield $name => ( new \ReflectionClass($field->getTypes()[0]) )->newInstanceWithoutConstructor();
@@ -129,9 +135,10 @@ class DatasetHandler
continue; 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 ( $entity->__isset($name) && ($relation->entity ?? $relation->bridge) !== static::class ) {
if ( null !== $value = $entity->__isset($name) ?? null ) { $value = $entity->$name ?? null;
if ( null !== $value ) {
if ( is_iterable($value) ) { if ( is_iterable($value) ) {
$list = []; $list = [];
+2 -2
View File
@@ -13,12 +13,12 @@ interface EntityInterface extends SearchableInterface /* extends \JsonSerializab
{ {
public function fromArray(iterable $dataset) : static; public function fromArray(iterable $dataset) : static;
public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false) : array; public function entityGetDataset(bool $includeRelations = false, bool $returnSource = false) : array;
public function toArray($includeRelations = false, array $filterFields = null) : array; public function toArray($includeRelations = false, array $filterFields = []) : array;
public function toCollection() : EntityCollection; public function toCollection() : EntityCollection;
public function isLoaded() : bool; public function isLoaded() : bool;
public function jsonSerialize() : mixed; public function jsonSerialize() : mixed;
public static function resolveEntity() : EntityResolver; public static function resolveEntity() : EntityResolver;
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository; public static function repository(string $alias = Repository::DEFAULT_ALIAS, ?ConnectionAdapter $adapter = null) : Repository;
public static function entityCollection(...$arguments) : EntityCollection; public static function entityCollection(...$arguments) : EntityCollection;
public static function queryBuilder() : QueryBuilderInterface; public static function queryBuilder() : QueryBuilderInterface;
public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField; public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField;
+1 -1
View File
@@ -3,6 +3,6 @@
namespace Ulmus\Entity; namespace Ulmus\Entity;
interface EntityObjectInterface { interface EntityObjectInterface {
public function load(...$arguments); public function load(mixed ...$arguments);
public function save(); public function save();
} }
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Entity;
interface EntityValueModifier {
public function push(mixed $value, \Notes\Common\ReflectedProperty $property) : mixed;
public function pull(mixed $value, \Notes\Common\ReflectedProperty $property) : mixed;
}
+22 -2
View File
@@ -4,11 +4,11 @@ namespace Ulmus\Entity\Field;
use Ulmus\Entity\EntityObjectInterface; use Ulmus\Entity\EntityObjectInterface;
class Datetime extends \DateTime implements EntityObjectInterface { class Datetime extends \DateTime implements EntityObjectInterface, \JsonSerializable {
public string $format = "Y-m-d H:i:s"; public string $format = "Y-m-d H:i:s";
public function load(...$arguments) public function load(mixed ...$arguments)
{ {
$value = $arguments[0]; $value = $arguments[0];
@@ -28,6 +28,13 @@ class Datetime extends \DateTime implements EntityObjectInterface {
return $obj; return $obj;
} }
public function setTime($hour, $minute, $second = 0, $microsecond = 0) : \DateTime
{
$fromParent = parent::setTime($hour, $minute, $second, $microsecond);
return new static($fromParent);
}
public function save() public function save()
{ {
return $this->__toString(); return $this->__toString();
@@ -38,13 +45,26 @@ class Datetime extends \DateTime implements EntityObjectInterface {
return $this->format($this->format); return $this->format($this->format);
} }
#[\Deprecated]
public function formatLocale(string $format) : string public function formatLocale(string $format) : string
{ {
return strftime($format, $this->getTimestamp()); return strftime($format, $this->getTimestamp());
} }
public function formatLocaleIntl(int $dateFormatter = \IntlDateFormatter::LONG, int $timeFormatter = \IntlDateFormatter::NONE, ? string $pattern = null) : string
{
$formatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormatter, $timeFormatter, \date_default_timezone_get(), \IntlDateFormatter::GREGORIAN, $pattern);
return $formatter->format($this->getTimestamp());
}
public function ageAt(null|\DateTime $dateTime) : int public function ageAt(null|\DateTime $dateTime) : int
{ {
return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000; return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000;
} }
public function jsonSerialize(): mixed
{
return $this;
}
} }
+14 -22
View File
@@ -5,17 +5,14 @@ namespace Ulmus\Entity\InformationSchema;
use Notes\Common\ReflectedProperty; use Notes\Common\ReflectedProperty;
use Ulmus\Entity\Field\Datetime; use Ulmus\Entity\Field\Datetime;
use Ulmus\{Attribute\Obj\Table}; use Ulmus\{Attribute\Obj\Table, Entity\EntityInterface};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
#[Table(name: "columns", database: "information_schema")] #[Table(name: "columns", database: "information_schema")]
class Column class Column implements EntityInterface
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
#[Field\Id]
public ? id $srs_id;
#[Field(name: "TABLE_CATALOG", length: 512)] #[Field(name: "TABLE_CATALOG", length: 512)]
public string $tableCatalog; public string $tableCatalog;
@@ -64,38 +61,27 @@ class Column
# #[Field(name: "COLLATION_TYPE", type: "longtext")] # #[Field(name: "COLLATION_TYPE", type: "longtext")]
# public string $type; # public string $type;
#[Field(name: "COLUMN_KEY", length: 3)]
public string $key;
#[Field(name: "EXTRA", length: 30)]
public string $extra;
#[Field(name: "PRIVILEGES", length: 80)]
public string $privileges;
#[Field(name: "COLUMN_COMMENT", length: 1024)]
public string $comment;
# #[Field(name: "IS_GENERATED", length: 6)] # #[Field(name: "IS_GENERATED", length: 6)]
# public string $generated; # public string $generated;
#[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
public ? string $generationExpression;
public function matchFieldDefinition(ReflectedProperty $definition) : bool public function matchFieldDefinition(ReflectedProperty $definition) : bool
{ {
$nullable = $this->nullable === 'YES'; $nullable = $this->nullable === 'YES';
$definitionValue = $this->getDefinitionValue($definition);
if ($nullable !== $definition->allowsNull()) { if ($nullable !== $definition->allowsNull()) {
return false; return false;
} }
if ( isset($definition->value) && $this->canHaveDefaultValue() ) { if ( isset($definitionValue) && $this->canHaveDefaultValue() ) {
if ( $definition->value !== $this->defaultValue()) { if ( $definitionValue !== $this->defaultValue() ) {
# dump($definition->value, $this->defaultValue(), $definition->name);
return false; return false;
} }
} }
elseif (! isset($definition->value)) { elseif (! isset($definitionValue)) {
if ( ! $this->defaultValueIsNull() ) { if ( ! $this->defaultValueIsNull() ) {
return false; return false;
} }
@@ -104,6 +90,12 @@ class Column
return true; return true;
} }
protected function getDefinitionValue(ReflectedProperty $definition) : mixed
{
# Attribute's value first, then defined in class value
return $definition->getAttribute(Field::class)->object->default ?? $definition->value ?? null;
}
protected function defaultValueIsNull() : bool protected function defaultValueIsNull() : bool
{ {
return $this->defaultValue() === null; return $this->defaultValue() === null;
+1 -1
View File
@@ -9,7 +9,7 @@ use Ulmus\{Attribute\Obj\Table as TableObj, Entity\EntityInterface};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
#[TableObj(name: "tables", database: "information_schema")] #[TableObj(name: "tables", database: "information_schema")]
class Table class Table implements EntityInterface
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Entity;
interface JsonUnserializable extends \JsonSerializable
{
public function jsonUnserialize(array $json): void;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Ulmus\Entity\Mariadb;
use Ulmus\Attribute\Property\Field;
class Column extends \Ulmus\Entity\Mysql\Column
{
#[Field\Id]
public ? id $srs_id;
#[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
public ? string $generationExpression;
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace Ulmus\Entity\Mssql;
use Ulmus\Attribute\Obj\Table as TableObj;
use Ulmus\Attribute\Property\Field;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\id;
use Ulmus\Repository;
use Ulmus\Ulmus;
#[TableObj(name: "columns", schema: "information_schema")]
class Column extends \Ulmus\Entity\InformationSchema\Column
{
/*
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null): Repository
{
$adapter = Ulmus::$registeredAdapters[$this->loadedFromAdapter];
return Ulmus::repository(static::class, $alias, $adapter);
}*/
# 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'
]);
}
protected function defaultValue() : mixed
{
if ($this->default === null) {
return null;
}
# Removing first pairs of brackets
$default = $this->isBetween($this->default, '(', ')') ? substr($this->default, 1, -1) : $this->default;
if ($default === "NULL") {
return null;
}
# Checking if another pairs of brackets surrounds the value
if ($this->isBetween($default, '(', ')')) {
$default = substr($default, 1, -1);
if (is_numeric($default)) {
return (int) $default;
}
}
elseif ($this->isBetween($default, "'", "'")) {
$default = new class($default) implements \Stringable {
public function __construct(
private readonly string $default
) {}
public function __toString(): string
{
return substr($this->default, 1, -1);;
}
};
}
else {
$default = new class($default) implements \Stringable {
public function __construct(
private readonly string $default
) {}
public function __toString(): string
{
return $this->default;
}
};
}
return (string) $default;
}
private function isBetween(string $str, string $first, string $last) : bool
{
return str_starts_with($str, $first) && str_ends_with($str, $last);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Ulmus\Entity\Mssql;
use Ulmus\EntityCollection,
Ulmus\Entity\Field\Datetime;
use Ulmus\{Attribute\Obj\Table as TableObj, Ulmus};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
use Ulmus\Repository;
#[TableObj(name: "tables", schema: "information_schema")]
class Table extends \Ulmus\Entity\InformationSchema\Table
{
#[Virtual(method: "filterColumns")]
public EntityCollection $columns;
public function filterColumns() : EntityCollection
{
$adapter = Ulmus::$registeredAdapters[$this->loadedFromAdapter];
return Column::repository(Repository\MssqlRepository::DEFAULT_ALIAS, $adapter)
->select('*')
->where('TABLE_NAME', $this->name )
->and('TABLE_SCHEMA', $this->schema )
->loadAll();
}
}
+21
View File
@@ -2,8 +2,28 @@
namespace Ulmus\Entity\Mysql; namespace Ulmus\Entity\Mysql;
use Ulmus\Attribute\Property\Field;
class Column extends \Ulmus\Entity\InformationSchema\Column class Column extends \Ulmus\Entity\InformationSchema\Column
{ {
##[Field\Id]
#public ? id $srs_id;
#[Field(name: "COLUMN_KEY", length: 3)]
public string $key;
#[Field(name: "EXTRA", length: 30)]
public string $extra;
#[Field(name: "PRIVILEGES", length: 80)]
public string $privileges;
#[Field(name: "COLUMN_COMMENT", length: 1024)]
public string $comment;
##[Field(name: "GENERATION_EXPRESSION", type: "longtext")]
#public ? string $generationExpression;
# TODO ! Handle FUNCTIONAL default value # TODO ! Handle FUNCTIONAL default value
protected function canHaveDefaultValue(): bool protected function canHaveDefaultValue(): bool
{ {
@@ -12,4 +32,5 @@ class Column extends \Ulmus\Entity\InformationSchema\Column
]); ]);
} }
} }
+40 -8
View File
@@ -5,17 +5,43 @@ namespace Ulmus\Entity;
class ObjectInstanciator { class ObjectInstanciator {
protected array $objectCallbackDefinition; protected array $objectCallbackDefinition;
public function instanciate(string $type, array $arguments) : object public function instanciate(string $type, array $arguments) : object
{ {
if ( isset($this->objectCallbackDefinition[$type]) ) { if (is_object($arguments[0]) && is_a($arguments[0], $type)) {
return $arguments[0];
}
elseif ( isset($this->objectCallbackDefinition[$type]) ) {
return $this->objectCallbackDefinition[$type](...$arguments); return $this->objectCallbackDefinition[$type](...$arguments);
} }
elseif ( ($obj = new $type() ) instanceof EntityObjectInterface ) {
return $obj->load(...$arguments);
}
else { else {
return new $type(...$arguments); $reflect = new \ReflectionClass($type);
if ( is_a($type, EntityObjectInterface::class, true) ) {
try {
$obj = $reflect->newInstance();
}
catch(\Throwable $t) {
$obj = $reflect->newInstanceWithoutConstructor();
}
return $obj->load(...$arguments);
}
elseif ( is_a($type, JsonUnserializable::class, true)) {
try {
$obj = $reflect->newInstance();
}
catch(\Throwable $t) {
$obj = $reflect->newInstanceWithoutConstructor();
}
$obj->jsonUnserialize(json_decode($arguments[0], true));
return $obj;
}
else {
return new $type(...$arguments);
}
} }
} }
@@ -24,8 +50,14 @@ class ObjectInstanciator {
if ( $obj instanceof EntityObjectInterface ) { if ( $obj instanceof EntityObjectInterface ) {
return $obj->save(); return $obj->save();
} }
elseif ( $obj instanceof \JsonSerializable ) {
return (string) $obj; return json_encode($obj->jsonSerialize());
}
elseif ( $obj instanceof \Stringable ) {
return (string) $obj;
}
throw new \InvalidArgumentException("Object %s could not be converted as an acceptable format.");
} }
public function enum(\UnitEnum $obj) public function enum(\UnitEnum $obj)
+2 -2
View File
@@ -5,11 +5,11 @@ namespace Ulmus\Entity\Sqlite;
use Notes\Common\ReflectedProperty; use Notes\Common\ReflectedProperty;
use Ulmus\EntityCollection; use Ulmus\EntityCollection;
use Ulmus\{Attribute\Obj\Table}; use Ulmus\{Attribute\Obj\Table, Entity\EntityInterface};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
#[Table] #[Table]
class Column class Column implements EntityInterface
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
+2 -2
View File
@@ -4,11 +4,11 @@ namespace Ulmus\Entity\Sqlite;
use Ulmus\EntityCollection; use Ulmus\EntityCollection;
use Ulmus\Query\{From, Select}; use Ulmus\Query\{From, Select};
use Ulmus\{Attribute\Obj\Table, Repository, Ulmus}; use Ulmus\{Attribute\Obj\Table, Entity\EntityInterface, Repository, Ulmus};
use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where}; use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual, Where};
#[Table(name: "sqlite_master")] #[Table(name: "sqlite_master")]
class Schema class Schema implements EntityInterface
{ {
use \Ulmus\EntityTrait; use \Ulmus\EntityTrait;
+1 -1
View File
@@ -9,7 +9,7 @@ use Ulmus\Repository;
#[\Ulmus\Attribute\Obj\Table(name: "sqlite_master")] #[\Ulmus\Attribute\Obj\Table(name: "sqlite_master")]
class Table extends Schema class Table extends Schema
{ {
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null): Repository public static function repository(string $alias = Repository::DEFAULT_ALIAS, ?ConnectionAdapter $adapter = null): Repository
{ {
$repository = new class(static::class, $alias, $adapter) extends Repository\SqliteRepository $repository = new class(static::class, $alias, $adapter) extends Repository\SqliteRepository
{ {
+48 -10
View File
@@ -11,11 +11,29 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
public static function instance(array $data = [], null|string $entityClass = null) : self public static function instance(array $data = [], null|string $entityClass = null) : self
{ {
$instance = new static($data); $instance = new static($data);
$instance->entityClass = $entityClass;
if ($entityClass) {
$instance->defineEntityClass($entityClass);
}
return $instance; return $instance;
} }
public function defineEntityClass(string $entityClass) : self
{
if ($this->entityClass !== null) {
throw new \LogicException("This methods should only be used to convert instantiated EntityCollection outside of entities.");
}
$this->entityClass = $entityClass;
foreach($this as &$arr) {
$arr = new $entityClass($arr);
}
return $this;
}
public function filters(Callable $callback, bool $yieldValueOnly = false) : Generator public function filters(Callable $callback, bool $yieldValueOnly = false) : Generator
{ {
$idx = 0; $idx = 0;
@@ -100,7 +118,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return $this; return $this;
} }
public function removeOne($value, string $field, bool $strict = true) : ? object public function removeOne($value, callable|string $field, bool $strict = true) : ? object
{ {
foreach($this->search($value, $field, $strict) as $key => $item) { foreach($this->search($value, $field, $strict) as $key => $item) {
$this->offsetUnset($key); $this->offsetUnset($key);
@@ -111,7 +129,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return null; return null;
} }
public function remove(/* mixed */ $value, string $field, bool $strict = true) : array public function remove(/* mixed */ $value, callable|string $field, bool $strict = true) : array
{ {
$removed = []; $removed = [];
@@ -131,14 +149,21 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return $this; return $this;
} }
public function search(mixed $value, string $field, bool $strict = true) : Generator public function search(mixed $value, callable|string $field, bool $strict = true) : Generator
{ {
foreach($this->filters(fn($v) => isset($v->$field) ? ( $strict ? $v->$field === $value : $v->$field == $value ) : false) as $key => $item) { if (is_string($field)) {
$filtering = fn($v) => isset($v->$field) ? ($strict ? $v->$field === $value : $v->$field == $value) : false;
}
elseif (is_callable($field)) {
$filtering = fn($v) => ( $fieldValue = call_user_func($field, $v) ) !== null ? ($strict ? $fieldValue === $value : $fieldValue == $value) : false;
}
foreach($this->filters($filtering) as $key => $item) {
yield $key => $item; yield $key => $item;
} }
} }
public function searchOne(mixed $value, string $field, bool $strict = true) : ? object public function searchOne(mixed $value, callable|string $field, bool $strict = true) : ? object
{ {
# Returning first value only # Returning first value only
foreach($this->search($value, $field, $strict) as $item) { foreach($this->search($value, $field, $strict) as $item) {
@@ -148,7 +173,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return null; return null;
} }
public function searchAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self public function searchAll(mixed $values, callable|string $field, bool $strict = true, bool $compareArray = false) : self
{ {
$collection = new static(); $collection = new static();
@@ -163,7 +188,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
return $collection; return $collection;
} }
public function diffAll(mixed $values, string $field, bool $strict = true, bool $compareArray = false) : self public function diffAll(mixed $values, callable|string $field, bool $strict = true, bool $compareArray = false) : self
{ {
$obj = new static($this->getArrayCopy()); $obj = new static($this->getArrayCopy());
@@ -343,7 +368,20 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
$list[] = $entity instanceof \JsonSerializable ? $entity->jsonSerialize() : $entity->toArray($includeRelations); $list[] = $entity instanceof \JsonSerializable ? $entity->jsonSerialize() : $entity->toArray($includeRelations);
} }
return $list; return $this->utf8ize($list, "UTF-8", "UTF-8");
}
protected function utf8ize(mixed $content) {
if (is_array($content)) {
foreach ($content as $key => $value) {
$content[$key] = $this->utf8ize($value);
}
}
elseif (is_string($content)) {
return mb_convert_encoding($content, "UTF-8", "UTF-8");
}
return $content;
} }
public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self public function fromArray(array $datasets, ? string /*stringable*/ $entityClass = null) : self
@@ -363,7 +401,7 @@ class EntityCollection extends \ArrayObject implements \JsonSerializable {
$className = $entityClass ?: $this->entityClass; $className = $entityClass ?: $this->entityClass;
return ( new $className() )->fromArray($dataset); return new $className($dataset) ;
} }
public function arrayToEntities(array $list, ? string /*stringable*/ $entityClass = null) : self public function arrayToEntities(array $list, ? string /*stringable*/ $entityClass = null) : self
+141 -38
View File
@@ -3,22 +3,18 @@
namespace Ulmus; namespace Ulmus;
use Notes\Attribute\Ignore; use Notes\Attribute\Ignore;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\{Attribute\Property\Join, use Ulmus\{Attribute\Obj\JsonSerialize,
Attribute\Property\Relation,
Attribute\Property\ResettablePropertyInterface, Attribute\Property\ResettablePropertyInterface,
Attribute\Property\Virtual,
Common\EntityResolver, Common\EntityResolver,
Common\EntityField, Common\EntityField,
Entity\DatasetHandler, Entity\DatasetHandler,
Entity\EntityInterface, Entity\EntityInterface,
QueryBuilder\QueryBuilderInterface}; QueryBuilder\QueryBuilderInterface};
use Ulmus\SearchRequest\{Attribute\SearchParameter,
SearchMethodEnum,
SearchRequestInterface,
SearchRequestFromRequestTrait,
SearchRequestPaginationTrait};
use Ulmus\{Attribute\RegisterEntityEventInterface, Entity\EntityValueModifier, };
#[JsonSerialize(includeRelations: true)]
trait EntityTrait { trait EntityTrait {
use EventTrait; use EventTrait;
@@ -32,7 +28,10 @@ trait EntityTrait {
protected array $entityDatasetUnmatchedFields = []; protected array $entityDatasetUnmatchedFields = [];
#[Ignore] #[Ignore]
protected DatasetHandler $datasetHandler; public DatasetHandler $datasetHandler;
#[Ignore]
public string $loadedFromAdapter;
#[Ignore] #[Ignore]
public function __construct(iterable|null $dataset = null) public function __construct(iterable|null $dataset = null)
@@ -43,27 +42,60 @@ trait EntityTrait {
#[Ignore] #[Ignore]
public function initializeEntity(iterable|null $dataset = null) : void public function initializeEntity(iterable|null $dataset = null) : void
{ {
# Attributs can attach themselves on events thrown by this trait
$this->entityRegisterAttributes();
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
if ($dataset) { if ($dataset) {
$this->fromArray($dataset); $this->fromArray($dataset);
} }
$this->datasetHandler = new DatasetHandler(static::resolveEntity(), $this->entityStrictFieldsDeclaration);
$this->resetVirtualProperties(); $this->resetVirtualProperties();
} }
#[Ignore]
public function entityRegisterAttributes() : void
{
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) {
if ( $tag->object instanceof RegisterEntityEventInterface ) {
$tag->object->attach($this, $field);
}
}
}
}
#[Ignore] #[Ignore]
public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self public function entityFillFromDataset(iterable $dataset, bool $overwriteDataset = false) : self
{ {
$properties = $this->resolveEntity()->reflectedClass->getProperties(true);
$loaded = $this->isLoaded(); $loaded = $this->isLoaded();
$handler = $this->datasetHandler->push($dataset); $generator = $this->datasetHandler->push($dataset);
foreach($generator as $field => $value) {
foreach($properties[$field]->attributes as $tag) {
if ( $tag->object instanceof EntityValueModifier ) {
try {
$value = $tag->object->push($value, $properties[$field]);
}
catch(\Throwable $ex) {
throw new \LogicException(
message: sprintf("An error occured trying to push an EntityValueModifier results : %s", $ex->getMessage()),
code: $ex->getCode(),
previous: $ex,
);
}
}
}
foreach($handler as $field => $value) {
$this->$field = $value; $this->$field = $value;
} }
$this->entityDatasetUnmatchedFields = $handler->getReturn(); $this->eventExecute(Event\Entity\DatasetFill::class, $this);
$this->entityDatasetUnmatchedFields = $generator->getReturn();
# Keeping original data to diff on UPDATE query # Keeping original data to diff on UPDATE query
if ( ! $loaded ) { if ( ! $loaded ) {
@@ -72,7 +104,7 @@ trait EntityTrait {
elseif ($overwriteDataset) { elseif ($overwriteDataset) {
$this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER) + $this->entityLoadedDataset; $this->entityLoadedDataset = array_change_key_case(is_array($dataset) ? $dataset : iterator_to_array($dataset), \CASE_LOWER) + $this->entityLoadedDataset;
} }
return $this; return $this;
} }
@@ -83,19 +115,33 @@ trait EntityTrait {
return $this->entityLoadedDataset; return $this->entityLoadedDataset;
} }
$dataset = []; return iterator_to_array($this->entityYieldDataset($includeRelations, $rewriteValue));
}
#[Ignore]
protected function entityYieldDataset(bool $includeRelations = false, bool $rewriteValue = true) : \Generator
{
foreach($this->datasetHandler->pull($this) as $field => $value) { foreach($this->datasetHandler->pull($this) as $field => $value) {
$dataset[$field] = $rewriteValue ? static::repository()->adapter->adapter()->writableValue($value) : $value; yield $field => $rewriteValue ? static::repository()->adapter->adapter()->writableValue($value) : $value;
} }
if ($includeRelations) { if ($includeRelations) {
foreach($this->datasetHandler->pullRelation($this) as $field => $object) { foreach($this->datasetHandler->pullRelation($this) as $field => $object) {
$dataset[$field] = $object; yield $field => $object;
} }
} }
}
return $dataset; #[Ignore]
public function entityGetDiffDataset() : array
{
$return = [];
foreach($this->datasetHandler->pull($this, false) as $field => $value) {
$return[$field] = static::repository()->adapter->adapter()->writableValue($value);
}
return $return;
} }
#[Ignore] #[Ignore]
@@ -103,7 +149,6 @@ trait EntityTrait {
{ {
foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) { foreach($this->resolveEntity()->reflectedClass->getProperties(true) as $field => $property) {
foreach($property->attributes as $tag) { foreach($property->attributes as $tag) {
if ( $tag->object instanceof ResettablePropertyInterface ) { if ( $tag->object instanceof ResettablePropertyInterface ) {
unset($this->$field); unset($this->$field);
} }
@@ -124,7 +169,7 @@ trait EntityTrait {
} }
#[Ignore] #[Ignore]
public function toArray($includeRelations = false, array $filterFields = null, bool $rewriteValue = true) : array public function toArray($includeRelations = false, array $filterFields = [], bool $rewriteValue = true) : array
{ {
$dataset = $this->entityGetDataset($includeRelations, false, $rewriteValue); $dataset = $this->entityGetDataset($includeRelations, false, $rewriteValue);
@@ -144,17 +189,31 @@ trait EntityTrait {
return false; return false;
} }
if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField($this) ) { if ( null === $pkField = $this->resolveEntity()->getPrimaryKeyField() ) {
throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class)); if ( null !== $compoundKeyFields = $this->resolveEntity()->getCompoundKeyFields() ) {
$loaded = false;
foreach ($compoundKeyFields->column as $column) {
$field = $this->resolveEntity()->field($column);
if (! $field->allowsNull() ) {
$loaded |= isset($this->{$field->name});
}
}
return $loaded;
};
}
else {
$key = key($pkField);
return isset($this->$key);
} }
$key = key($pkField); throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class));
return isset($this->$key);
} }
#[Ignore] #[Ignore]
public function __get(string $name) public function __get(string $name) : mixed
{ {
$relation = new Repository\RelationBuilder($this); $relation = new Repository\RelationBuilder($this);
@@ -170,11 +229,21 @@ trait EntityTrait {
{ {
$rel = static::resolveEntity()->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ]); $rel = static::resolveEntity()->searchFieldAnnotation($name, [ Attribute\Property\Relation::class ]);
if ( $this->isLoaded() && $rel ) { if ( $rel && $this->isLoaded() ) {
return true; return true;
} }
return isset($this->$name); if (isset($this->$name)) {
return true;
}
# Unexisting variable
if (! new \ReflectionClass($this)->hasProperty($name)) {
return false;
}
# Handling undefined 'nullable'
return new \ReflectionProperty($this, $name)->isInitialized($this);
} }
#[Ignore] #[Ignore]
@@ -202,7 +271,41 @@ trait EntityTrait {
#[Ignore] #[Ignore]
public function jsonSerialize() : mixed public function jsonSerialize() : mixed
{ {
return $this->entityGetDataset(true, false, false); # Allows overridding of this method
return $this->_jsonSerialize();
}
#[Ignore]
public function _jsonSerialize() : mixed
{
$resolver = static::resolveEntity();
$objectAttribute = $resolver->getAttributeImplementing(JsonSerialize::class);
$dataset = [];
foreach($this->entityYieldDataset($objectAttribute->includeRelations ?? true, false, false) as $key => $value) {
$field = $resolver->searchField($key, EntityResolver::KEY_COLUMN_NAME); # ?: $resolver->relation($key, false);
if ($field) {
$jsonSerialize = $field->getAttribute(\Ulmus\Attribute\Property\JsonSerialize::class);
if ($jsonSerialize) {
# Should we ignore the field ?
if ($jsonSerialize->object->ignoreField) {
continue;
}
}
}
if (is_object($value) && ($value instanceof \JsonSerializable) ) {
$dataset[$key] = $value->jsonSerialize();
}
else {
$dataset[$key] = $value;
}
}
return $dataset;
} }
#[Ignore] #[Ignore]
@@ -212,7 +315,7 @@ trait EntityTrait {
} }
#[Ignore] #[Ignore]
public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository public static function repository(string $alias = Repository::DEFAULT_ALIAS, ?ConnectionAdapter $adapter = null) : Repository
{ {
return Ulmus::repository(static::class, $alias, $adapter); return Ulmus::repository(static::class, $alias, $adapter);
} }
@@ -233,21 +336,21 @@ trait EntityTrait {
} }
#[Ignore] #[Ignore]
public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS, bool $select = false) : EntityField
{ {
$default = ( $alias === false ? '' : static::repository()::DEFAULT_ALIAS ); # bw compatibility, to be deprecated $default = ( $alias === false ? '' : static::repository()::DEFAULT_ALIAS ); # bw compatibility, to be deprecated
$alias = $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default; $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)); return new EntityField(
name: $name, entityClass: static::class, alias: $alias, entityResolver: Ulmus::resolveEntity(static::class), select: $select
);
} }
#[Ignore] #[Ignore]
public static function fields(array $fields, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : string public static function fields(array $fields, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ', bool $select = false) : string
{ {
return implode($separator, array_map(function($item) use ($alias){ return implode($separator, array_map(fn($e) => static::field($e, $alias, $select), $fields));
return static::field($item, $alias);
}, $fields));
} }
#[Ignore] #[Ignore]
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace Ulmus\Event\Entity;
interface DatasetFill {
public function execute(\Generator $generator, \Ulmus\Entity\EntityInterface $entity) : void;
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace Ulmus\Event\Entity;
interface DatasetPull {
public function execute(\Generator $generator, \Ulmus\Entity\EntityInterface $entity) : void;
}
+1 -1
View File
@@ -5,4 +5,4 @@ namespace Ulmus\Migration\Sql;
class SqliteMigration class SqliteMigration
{ {
} }
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace Ulmus\Migration;
use Ulmus\Adapter\AdapterInterface;
use Ulmus\Adapter\SqlFieldMapper;
use Ulmus\ConnectionAdapter;
use Ulmus\Entity\Mysql\Table;
use Ulmus\Repository;
trait SqlMigrationTrait
{
protected array $QUERY_REPLACE = [
'change' => "alter",
];
public function schemaTable(ConnectionAdapter $adapter, $databaseName, string $tableName) : null|object
{
return \Ulmus\Entity\InformationSchema\Table::repository(Repository::DEFAULT_ALIAS, $adapter)
->select(\Ulmus\Common\Sql::raw('this.*'))
->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->or($this->escapeIdentifier('table_catalog', AdapterInterface::IDENTIFIER_FIELD), $databaseName)
->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName);
}
public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string
{
$mapper = new SqlFieldMapper($field);
return $typeOnly ? $mapper->type : $mapper->render();
}
public function generateAlterColumn(FieldDefinition $definition, array $field) : string|\Stringable
{
return implode(" ", array_filter([
strtoupper($field['action']),
$this->escapeIdentifier($definition->getSqlName(), AdapterInterface::IDENTIFIER_FIELD),
$definition->getSqlType(),
$definition->getSqlParams(),
]));
}
public function splitAlterQuery() : bool
{
return false;
}
public function supportAlterColumnAction(string $action)
{
return in_array(strtolower($action), [ 'add', 'change' ]);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Pdo;
class Mssql extends \PDO
{
use PdoTrait;
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Pdo;
class Mysql extends \PDO\Mysql
{
use PdoTrait;
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace Ulmus\Pdo;
use PDO,
PDOStatement;
trait PdoTrait
{
public bool $executionStatus;
public int $rowCount = 0;
public mixed $lastInsertId = null;
public \Closure $onClose;
public function select(string $sql, array $parameters = []): PDOStatement
{
# static::$dump && call_user_func_array(static::$dump, [ $this->fillDebugSqlParameters($sql, $parameters) ]);
try {
if (false !== ( $statement = $this->prepare($sql) )) {
$statement->setFetchMode(\PDO::FETCH_ASSOC);
$this->execute($statement, $parameters, false);
}
}
catch (\Throwable $e) {
throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters));
}
return $statement;
}
public function __destruct()
{
if ($this->onClose ?? null) {
call_user_func($this->onClose, $this);
}
}
public function runQuery(string $sql, array $parameters = []): ? static
{
# static::$dump && call_user_func_array(static::$dump, [ $this->fillDebugSqlParameters($sql, $parameters) ]);
try {
if (false !== ( $statement = $this->prepare($sql) )) {
return $this->execute($statement, $parameters, true);
}
}
catch(\PDOException $pdo) {
if ( !str_starts_with($pdo->getMessage(), 'There is no active transaction') ) {
throw new \PdoException($pdo->getMessage() . " `$sql` with data:" . json_encode($parameters), (int) $pdo->getCode(), $pdo);
}
}
catch (\Throwable $e) {
throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters), (int) $e->getCode(), $e);
}
return null;
}
public function runInsertQuery(string $sql, array $parameters = []) : ? static
{
return $this->runQuery($sql, $parameters);
}
public function runUpdateQuery(string $sql, array $parameters = []) : ? static
{
return $this->runQuery($sql, $parameters);
}
public function runDeleteQuery(string $sql, array $parameters = []) : ? static
{
return $this->runQuery($sql, $parameters);
}
public function execute(PDOStatement $statement, array $parameters = [], bool $commit = true) : ? static
{
$this->executionStatus = false;
$this->lastInsertId = null;
try {
if (! $this->inTransaction()) {
$this->beginTransaction();
}
$this->bindVariables($statement, $parameters);
$this->executionStatus = $statement->execute();
if ( $this->executionStatus ) {
$this->lastInsertId = $this->lastInsertId();
$this->rowCount = $statement->rowCount();
if ( $commit ) {
$this->commit();
}
return $this;
}
else {
throw new \PDOException($statement->errorCode() . " - " . json_encode($statement->errorInfo()));
}
}
catch (\PDOException $e) {
$this->rollback();
throw $e;
}
}
protected function bindVariables(PDOStatement $statement, array &$parameters) : void
{
if ($parameters) {
if (array_is_list($parameters)) {
$parameters = array_combine(range(1, count($parameters)), array_values($parameters));
}
else {
foreach ($parameters as $key => $value) {
$type = match (strtolower(gettype($value))) {
"boolean", "integer" => Pdo::PARAM_INT,
"null" => Pdo::PARAM_NULL,
default => Pdo::PARAM_STR,
};
$statement->bindValue($key, $value, $type);
}
}
}
}
protected function fillDebugSqlParameters(string $sql, array $parameters) : string
{
$parameters = array_reverse($parameters);
return str_replace(array_keys($parameters), array_map(fn($e) => is_numeric($e) ? $e : (
is_bool($e) ? (int) $e : $this->quote($e)
), array_values($parameters)), $sql);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Ulmus\Pdo;
trait SqlPdoTrait
{
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 $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
}
public function rollback() : bool
{
if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
}
elseif ($this->openedTransaction-- === 1) {
return parent::rollback();
}
return false;
}
protected function openTransaction() : bool
{
return parent::beginTransaction();
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Ulmus\Pdo;
class Sqlite extends \PDO\Sqlite
{
use PdoTrait, SqlitePdoTrait;
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Ulmus\Pdo;
trait SqlitePdoTrait
{
use SqlPdoTrait;
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 $this->exec("RELEASE SAVEPOINT transaction_{$this->openedTransaction}") !== false;
}
public function rollback() : bool
{
if ($this->openedTransaction > 1) {
return $this->exec('ROLLBACK TO transaction_' . $this->openedTransaction--) !== false;
}
elseif ($this->openedTransaction-- === 1) {
# We must do it manually since opening a transaction manually stucks PDO into thinking we've got no transaction opened
return $this->exec('ROLLBACK') !== false;
}
return false;
}
protected function openTransaction(): bool
{
return $this->exec("BEGIN IMMEDIATE TRANSACTION") !== false;
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ abstract class Fragment implements QueryFragmentInterface{
return implode(", ", $list); return implode(", ", $list);
} }
protected function validateFieldType($field) : void protected function validateFieldType(mixed $field) : void
{ {
if ( is_numeric($field) ) { if ( is_numeric($field) ) {
throw new \Ulmus\Exception\InvalidFieldType(sprintf("Validation of field `%s` failed from query `%s`", $field, get_class($this))); throw new \Ulmus\Exception\InvalidFieldType(sprintf("Validation of field `%s` failed from query `%s`", $field, get_class($this)));
+10 -2
View File
@@ -10,6 +10,8 @@ class OrderBy extends Fragment {
const SQL_TOKEN = "ORDER BY"; const SQL_TOKEN = "ORDER BY";
const SQL_COLLATE = "COLLATE"; const SQL_COLLATE = "COLLATE";
# const 'NULLS LAST|FIRST' must be handlded
public function set(array $order) : self public function set(array $order) : self
{ {
$this->orderBy = $order; $this->orderBy = $order;
@@ -17,9 +19,9 @@ class OrderBy extends Fragment {
return $this; return $this;
} }
public function add(string|\Stringable $field, ? string $direction = null) : self public function add(string|\Stringable $field, string|\Stringable|null $direction = null) : self
{ {
$this->validateFieldType($field); $this->validateDirectionType($direction);
$this->orderBy[] = [ $field, $direction ]; $this->orderBy[] = [ $field, $direction ];
@@ -39,4 +41,10 @@ class OrderBy extends Fragment {
]); ]);
} }
protected function validateDirectionType(mixed $direction) {
# Could be coming from user-input, we must whitelist
if (is_string($direction) && ! in_array(strtoupper($direction), [ 'ASC', 'DESC' ])) {
throw new \Ulmus\Exception\InvalidFieldType(sprintf("Validation of string `%s` failed from query `%s`", $direction, get_class($this)));
}
}
} }
+2
View File
@@ -12,6 +12,8 @@ class Select extends Fragment {
public bool $union = false; public bool $union = false;
public bool $isInternalSelect = false;
public ? int $top = null; public ? int $top = null;
protected array $fields = []; protected array $fields = [];
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Ulmus\Query\Sqlite;
use Ulmus\Query\Fragment;
class Truncate extends Fragment {
const SQL_TOKEN = "DELETE FROM";
public int $order = -100;
public string $table;
public function set(string $tableName) : self
{
$this->table = $tableName;
return $this;
}
public function render() : string
{
return $this->renderSegments([
static::SQL_TOKEN, $this->table,
]);
}
}
+5 -1
View File
@@ -10,7 +10,7 @@ class Values extends Fragment {
public ? int $fieldCount = null; public ? int $fieldCount = null;
public array $rows; public array $rows = [];
public QueryBuilderInterface $queryBuilder; public QueryBuilderInterface $queryBuilder;
@@ -21,6 +21,10 @@ class Values extends Fragment {
public function add(array $row) : self public function add(array $row) : self
{ {
if ($row === []) {
return $this;
}
if ($this->fieldCount === null) { if ($this->fieldCount === null) {
$this->fieldCount = count($row); $this->fieldCount = count($row);
} }
+4 -3
View File
@@ -59,7 +59,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
$this->push($select); $this->push($select);
} }
$select->distinct = $distinct; $select->distinct = $select->distinct || $distinct;
return $this; return $this;
} }
@@ -491,7 +491,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
} }
} }
public function getFragments(QueryFragmentInterface|array|\Stringable|string $fragment = null) : array public function getFragments(QueryFragmentInterface|array|\Stringable|string|null $fragment = null) : array
{ {
return $fragment !== null ? array_filter($this->queryStack, fn($e) => is_a($e, $fragment, true) ) : $this->queryStack; return $fragment !== null ? array_filter($this->queryStack, fn($e) => is_a($e, $fragment, true) ) : $this->queryStack;
} }
@@ -501,7 +501,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
return $this->render(); return $this->render();
} }
public function addParameter(mixed $value, string $key = null) : string public function addParameter(mixed $value, ?string $key = null) : string
{ {
if ( $this->parent ?? false ) { if ( $this->parent ?? false ) {
return $this->parent->addParameter($value, $key); return $this->parent->addParameter($value, $key);
@@ -540,4 +540,5 @@ class MysqlQueryBuilder extends SqlQueryBuilder
return $next + 1; return $next + 1;
} }
} }
@@ -3,6 +3,7 @@
namespace Ulmus\QueryBuilder\Sql; namespace Ulmus\QueryBuilder\Sql;
use Ulmus\Query; use Ulmus\Query;
use Ulmus\Adapter\AdapterInterface;
# Soon to extends SqlQueryBuilder # Soon to extends SqlQueryBuilder
class SqliteQueryBuilder extends MysqlQueryBuilder class SqliteQueryBuilder extends MysqlQueryBuilder
@@ -38,4 +39,41 @@ class SqliteQueryBuilder extends MysqlQueryBuilder
return $this; return $this;
} }
public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null, bool $replace = false) : self
{
return parent::insert($fieldlist, $table, $alias, null, null, $replace);
}
public function update(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
return parent::update($table, $alias, null, null);
}
public function create(AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
return parent::create($adapter, $fieldlist, $table, null, null);
}
public function alter(AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self
{
return parent::alter($adapter, $fieldlist, $table, null, null);
}
public function from(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
return parent::from($table, $alias, null, null);
}
public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
{
$truncate = new Query\Sqlite\Truncate($this);
$this->push($truncate);
$truncate->set($table);
return $this;
}
} }
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Ulmus\QueryBuilder;
use Ulmus\Query\QueryFragmentInterface; use Ulmus\Query\QueryFragmentInterface;
# TODO -> Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder # TODO -> Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder
class SqlQueryBuilder implements QueryBuilderInterface abstract class SqlQueryBuilder implements QueryBuilderInterface
{ {
protected string $rendered; protected string $rendered;
+109 -52
View File
@@ -23,7 +23,7 @@ class Repository implements RepositoryInterface
public array $events = []; public array $events = [];
public ? ConnectionAdapter $adapters; public ? ConnectionAdapter $adapter;
public readonly QueryBuilder\QueryBuilderInterface $queryBuilder; public readonly QueryBuilder\QueryBuilderInterface $queryBuilder;
@@ -31,7 +31,8 @@ class Repository implements RepositoryInterface
protected array $joined = []; protected array $joined = [];
public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) { public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, null|ConnectionAdapter $adapter = null) {
$this->entityClass = $entity; $this->entityClass = $entity;
$this->alias = $alias; $this->alias = $alias;
$this->entityResolver = Ulmus::resolveEntity($entity); $this->entityResolver = Ulmus::resolveEntity($entity);
@@ -59,7 +60,7 @@ class Repository implements RepositoryInterface
public function loadOne() : ? object public function loadOne() : ? object
{ {
return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0] ?? null; return $this->limit(1)->loadAll()[0] ?? null;
} }
public function loadOneFromField($field, $value) : ? object public function loadOneFromField($field, $value) : ? object
@@ -72,11 +73,12 @@ class Repository implements RepositoryInterface
return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne(); return $primaryKey ? $this->loadOneFromField($primaryKey, $value) : $this->wherePrimaryKey($value)->loadOne();
} }
public function loadAll () : EntityCollection public function loadAll() : EntityCollection
{ {
return $this->selectSqlQuery()->collectionFromQuery(); return $this->selectSqlQuery()->collectionFromQuery();
} }
#[\Deprecated(message: "loadFromField is now to be used instead of this method")]
public function loadAllFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection public function loadAllFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection
{ {
return $this->loadFromField($field, $value, $operator); return $this->loadFromField($field, $value, $operator);
@@ -84,7 +86,7 @@ class Repository implements RepositoryInterface
public function loadFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection public function loadFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection
{ {
return $this->selectSqlQuery()->where($field, $value, $operator)->collectionFromQuery(); return $this->where($field, $value, $operator)->loadAll();
} }
public function count() : int public function count() : int
@@ -97,7 +99,7 @@ class Repository implements RepositoryInterface
else { else {
$pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); $pk = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if (count($pk) === 1) { if ($pk && count($pk) === 1) {
$field = key($pk); $field = key($pk);
$this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field)))); $this->select(Common\Sql::function('COUNT', Common\Sql::raw("DISTINCT " . $this->entityClass::field($field))));
@@ -140,6 +142,15 @@ class Repository implements RepositoryInterface
return (bool) $this->wherePrimaryKey($value, false)->deleteOne()->rowCount; return (bool) $this->wherePrimaryKey($value, false)->deleteOne()->rowCount;
} }
public function deleteFromCompoundKeys(array $values) : bool
{
foreach($values as $field => $value) {
$this->where($field, $value);
}
return (bool) $this->deleteOne()->rowCount;
}
public function destroy(object $entity) : bool public function destroy(object $entity) : bool
{ {
if ( ! $this->matchEntity($entity) ) { if ( ! $this->matchEntity($entity) ) {
@@ -148,16 +159,25 @@ class Repository implements RepositoryInterface
$primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField(); $primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
if ( $primaryKeyDefinition === null ) { if ( $primaryKeyDefinition !== null ) {
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass));
}
else {
$pkField = key($primaryKeyDefinition); $pkField = key($primaryKeyDefinition);
$this->eventExecute(Event\Query\Delete::class, $this, $entity); $this->eventExecute(Event\Query\Delete::class, $this, $entity);
return $this->deleteFromPk($entity->$pkField); return $this->deleteFromPk($entity->$pkField);
} }
else {
$compoundKeyFields = Ulmus::resolveEntity($this->entityClass)->getCompoundKeyFields();
if ($compoundKeyFields) {
$this->eventExecute(Event\Query\Delete::class, $this, $entity);
return $this->deleteFromCompoundKeys(array_combine($compoundKeyFields->column, array_map(fn($column) => $entity->$column, $compoundKeyFields->column)));
}
else {
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass));
}
}
} }
public function destroyAll(EntityCollection $collection) : void public function destroyAll(EntityCollection $collection) : void
@@ -181,7 +201,7 @@ class Repository implements RepositoryInterface
public function save(object|array $entity, ? array $fieldsAndValue = null, bool $replace = false) : bool public function save(object|array $entity, ? array $fieldsAndValue = null, bool $replace = false) : bool
{ {
if ( is_array($entity) ) { if ( is_array($entity) ) {
$entity = ( new $this->entityClass() )->fromArray($entity); $entity = new $this->entityClass($entity);
} }
if ( ! $this->matchEntity($entity) ) { if ( ! $this->matchEntity($entity) ) {
@@ -195,6 +215,10 @@ class Repository implements RepositoryInterface
if ( $replace || ! $entity->isLoaded() ) { if ( $replace || ! $entity->isLoaded() ) {
# $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH); # $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH);
if (empty($dataset)) {
throw new \InvalidArgumentException("Unable to add a new row from an empty dataset.");
}
$pdoObject = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery(); $pdoObject = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery();
if ( null !== $primaryKeyDefinition ) { if ( null !== $primaryKeyDefinition ) {
@@ -216,21 +240,29 @@ class Repository implements RepositoryInterface
} }
else { else {
if ( $primaryKeyDefinition === null ) { if ( $primaryKeyDefinition === null ) {
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass)); if ( null === $compoundKeyFields = Ulmus::resolveEntity($this->entityClass)->getCompoundKeyFields() ) {
throw new \Exception(sprintf("No primary key or compound key found for entity %s", $this->entityClass));
}
} }
$diff = $fieldsAndValue ?? $this->generateWritableDataset($entity); $diff = $fieldsAndValue ?? $this->generateWritableDataset($entity);
if ( [] !== $diff ) { if ( [] !== $diff ) {
$pkField = key($primaryKeyDefinition); if ($primaryKeyDefinition) {
$pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField; $pkField = key($primaryKeyDefinition);
$this->where($pkFieldName, $dataset[$pkFieldName]); $pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField;
$this->where($pkFieldName, $dataset[$pkFieldName]);
}
else {
foreach($compoundKeyFields->column as $field) {
$this->where($field, $dataset[$field]);
}
}
$update = $this->updateSqlQuery($diff)->runUpdateQuery(); $update = $this->updateSqlQuery($diff)->runUpdateQuery();
$entity->entityFillFromDataset($dataset, true); $entity->entityFillFromDataset($dataset, true);
# $fieldsAndValue ??= &$dataset;
$this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace); $this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $replace);
return $update ? (bool) $update->rowCount : false; return $update ? (bool) $update->rowCount : false;
@@ -305,7 +337,7 @@ class Repository implements RepositoryInterface
public function generateDatasetDiff(object $entity, bool $oldValues = false) : array public function generateDatasetDiff(object $entity, bool $oldValues = false) : array
{ {
$array = array_change_key_case($entity->toArray()); $array = array_change_key_case($entity->entityGetDiffDataset());
$dataset = array_change_key_case($entity->entityGetDataset(false, true)); $dataset = array_change_key_case($entity->entityGetDataset(false, true));
@@ -319,6 +351,18 @@ class Repository implements RepositoryInterface
} }
} }
if (is_null($e1) || is_null($e2)) {
return $e1 !== $e2;
}
if ($e1 instanceof \BackedEnum) {
$e1 = $e1->value;
}
if ($e2 instanceof \BackedEnum) {
$e2 = $e2->value;
}
return (string) $e1 !== (string) $e2; return (string) $e1 !== (string) $e2;
}); });
} }
@@ -355,6 +399,7 @@ class Repository implements RepositoryInterface
foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) { foreach ($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) { if (null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ])) {
# @TODO add handling of the new #[Select] attribute here !
$this->select(sprintf("%s.$key as {$prependField}{$field->name}", $this->escapeIdentifier($alias))); $this->select(sprintf("%s.$key as {$prependField}{$field->name}", $this->escapeIdentifier($alias)));
} }
} }
@@ -407,24 +452,24 @@ class Repository implements RepositoryInterface
public function withJoin(string|array $fields, array $options = []) : self public function withJoin(string|array $fields, array $options = []) : self
{ {
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { $canSelect = null === $this->queryBuilder->getFragment(Query\Select::class);
if ( $canSelect ) {
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); $select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select))); $this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true));
} }
# @TODO Apply FILTER annotation to this too ! $fields = (array) $fields;
foreach(array_filter((array) $fields) as $item) { usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.'));
if ( isset($this->joined[$item]) ) {
continue;
}
else {
$this->joined[$item] = true;
}
$attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?: # @TODO Apply fff annotation to this too !
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]); foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) {
$this->joined[$item] = true;
$isRelation = ( $attribute instanceof Relation ) || ($attribute instanceof Relation); $attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]));
$isRelation = $attribute instanceof Relation;
if ($isRelation && ( $attribute->isManyToMany() )) { if ($isRelation && ( $attribute->isManyToMany() )) {
throw new \Exception("Many-to-many relation can not be preloaded within joins."); throw new \Exception("Many-to-many relation can not be preloaded within joins.");
@@ -432,8 +477,7 @@ class Repository implements RepositoryInterface
if ( $attribute ) { if ( $attribute ) {
$alias = $attribute->alias ?? $item; $alias = $attribute->alias ?? $item;
$entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item);
$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) { foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) { if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
@@ -442,20 +486,23 @@ class Repository implements RepositoryInterface
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name; $name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
if ($canSelect) {
# @TODO add handling of the new #[Select] attribute here !
$this->select("$escAlias.$fieldName as $alias\${$name}"); $this->select("$escAlias.$fieldName as $alias\${$name}");
}
} }
} }
$this->open(); $this->open();
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) { if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) { foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) { 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); $this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
}
} }
} }
}
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) { if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) { foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
@@ -517,7 +564,11 @@ class Repository implements RepositoryInterface
} }
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$this->select($this->escapeIdentifier($this->alias) . ".*"); # @TODO add handling of the new #[Select] attribute here !
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), alias: $this->alias, select: true));
# $this->select($this->escapeIdentifier($this->alias) . ".*");
} }
# Apply FILTER annotation to this too ! # Apply FILTER annotation to this too !
@@ -635,20 +686,18 @@ class Repository implements RepositoryInterface
public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest, bool $count = true) : self
{ {
if ($count) { if ($count) {
$searchRequest->count = $searchRequest->filter($this->serverRequestCountRepository()) $searchRequest->applyCount($this->serverRequestCountRepository());
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR) # @TODO Handle no request to do if count is 0 here
->groups($searchRequest->groups()) #if ($searchRequest->count) {
->count(); $searchRequest->apply($this);
# }
}
else {
$searchRequest->apply($this);
} }
return $searchRequest->filter($this) return $this;
->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
->orders($searchRequest->orders())
->groups($searchRequest->groups())
->offset($searchRequest->offset())
->limit($searchRequest->limit());
} }
protected function serverRequestCountRepository() : Repository protected function serverRequestCountRepository() : Repository
@@ -734,9 +783,17 @@ class Repository implements RepositoryInterface
return $this->entityClass::entityCollection(...$arguments); return $this->entityClass::entityCollection(...$arguments);
} }
public function instanciateEntity(? string $entityClass = null) : object public function instanciateEntity(? string $entityClass = null, array $dataset = []) : object
{ {
$entity = ( new \ReflectionClass($entityClass ?? $this->entityClass) )->newInstanceWithoutConstructor(); $entityClass ??= $this->entityClass;
try {
$entity = new $entityClass($dataset);
}
catch (\Throwable $ex) {
$entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor()->fromArray($dataset);
}
$entity->initializeEntity(); $entity->initializeEntity();
return $entity; return $entity;
+3 -3
View File
@@ -32,10 +32,10 @@ trait ConditionTrait
return $this; return $this;
} }
public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self public function wheres(array $fieldValues, string|array $operator = Query\Where::OPERATOR_EQUAL, string|array $condition = Query\Where::CONDITION_AND) : self
{ {
foreach($fieldValues as $field => $value) { foreach($fieldValues as $field => $value) {
$this->where($field, $value, $operator); $this->where($field, $value, is_array($operator) ? ( $operator[$field] ?? Query\Where::OPERATOR_EQUAL ) : $operator, is_array($condition) ? ( $condition[$field] ?? Query\Where::CONDITION_AND ) : $condition);
} }
return $this; return $this;
@@ -142,7 +142,7 @@ trait ConditionTrait
return $this; return $this;
} }
public function likes(array $fieldValues, string $condition = Query\Where::CONDITION_AND) : self public function likes(array $fieldValues, string|array $condition = Query\Where::CONDITION_AND) : self
{ {
foreach($fieldValues as $field => $value) { foreach($fieldValues as $field => $value) {
$this->like($field, $value); $this->like($field, $value);
+19 -3
View File
@@ -2,19 +2,35 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{Repository, Query, Ulmus, Common}; use Ulmus\{EntityCollection, Repository, Query, Entity, Ulmus, Common};
class MssqlRepository extends Repository { class MssqlRepository extends Repository {
protected function finalizeQuery() : void protected function finalizeQuery() : void
{ {
if ( $this->queryBuilder->getFragment(Query\MsSQL\Offset::class) ) { $delete = $this->queryBuilder->getFragment(Query\Delete::class);
if (null === $order = $this->queryBuilder->getFragment(Query\OrderBy::class)) { $offset = $this->queryBuilder->getFragment(Query\MsSQL\Offset::class);
if ($offset) {
if ( $delete ) {
$delete->top = $offset->limit;
$this->queryBuilder->removeFragment(Query\MsSQL\Offset::class);
}
elseif (null === $this->queryBuilder->getFragment(Query\OrderBy::class)) {
$this->orderBy("(SELECT 0)"); $this->orderBy("(SELECT 0)");
} }
} }
} }
public function listColumns(? string $table = null) : EntityCollection
{
$table ??= $this->entityResolver->tableName();
$this->showColumnsSqlQuery($table);
return $this->collectionFromQuery(Entity\Mssql\Column::class)->iterate(fn($e) => $e->tableName = $table);
}
protected function serverRequestCountRepository() : Repository protected function serverRequestCountRepository() : Repository
{ {
return new static($this->entityClass, $this->alias, $this->adapter); return new static($this->entityClass, $this->alias, $this->adapter);
+9 -1
View File
@@ -2,7 +2,7 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{ Repository, Query, Ulmus}; use Ulmus\{EntityCollection, Repository, Query, Entity, Ulmus};
class MysqlRepository extends Repository { class MysqlRepository extends Repository {
use JsonConditionTrait; use JsonConditionTrait;
@@ -14,6 +14,14 @@ class MysqlRepository extends Repository {
return $this; return $this;
} }
public function listColumns(? string $table = null) : EntityCollection
{
$table ??= $this->entityResolver->tableName();
$this->showColumnsSqlQuery($table);
return $this->collectionFromQuery(Entity\Mysql\Column::class)->iterate(fn($e) => $e->tableName = $table);
}
public function createSqlQuery() : self public function createSqlQuery() : self
{ {
if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) { if ( null === $this->queryBuilder->getFragment(Query\Engine::class) ) {
+1 -1
View File
@@ -222,7 +222,7 @@ trait QueryBuildingTrait
{ {
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
$fields = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true); $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(fields: array_map(fn($f) => $f->object->name ?? $f->name, $fields), select: true));
} }
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) { if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {
+54 -18
View File
@@ -3,12 +3,10 @@
namespace Ulmus\Repository; namespace Ulmus\Repository;
use Ulmus\{ use Ulmus\{
Ulmus, Query, Common\EntityResolver, Repository, Event, Ulmus, Query, Common\EntityResolver, Repository, Event, Common\Sql
}; };
use Ulmus\Attribute\Property\{ use Ulmus\Attribute\Property\{Filter, Join, Limit, Offset, OrderBy, Relation, Virtual, Where, WithJoin};
Filter, Join, OrderBy, Relation, Virtual, Where, WithJoin,
};
class RelationBuilder class RelationBuilder
{ {
@@ -30,6 +28,10 @@ class RelationBuilder
protected array $joins; protected array $joins;
protected ? Limit $limit;
protected ? Offset $offset;
public function __construct(string|object $entity, ? Repository $repository = null) public function __construct(string|object $entity, ? Repository $repository = null)
{ {
$this->entity = $entity; $this->entity = $entity;
@@ -57,7 +59,7 @@ class RelationBuilder
} }
else { else {
if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) { if ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class, Join::class ] ) ) {
return $this->instanciateEmptyObject($name, $relation); return $this->instanciateEmptyObject($name, $relation);
} }
elseif ($virtual = $this->resolveVirtual($name)) { elseif ($virtual = $this->resolveVirtual($name)) {
@@ -82,7 +84,8 @@ class RelationBuilder
} }
} }
catch(\Throwable $e) { catch(\Throwable $e) {
throw new $e(sprintf("An error occurred while calling method from your #[Virtual] attribute in entity '%s::%s'. Caught '%s'", $this->entity::class, $name, $e->getMessage() ));
throw new $e(sprintf("An error occurred while calling method from your #[Virtual] attribute in entity '%s::%s'. Caught '%s'", $this->entity::class, $name, $e->getMessage(), $e->getCode(), $e ));
} }
} }
@@ -93,6 +96,8 @@ class RelationBuilder
{ {
if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) ) { if ( null !== ( $relation = $this->resolver->searchFieldAnnotation($name, [ Relation::class ] ) ) ) {
$this->orders = $this->resolver->searchFieldAnnotationList($name, [ OrderBy::class ] ); $this->orders = $this->resolver->searchFieldAnnotationList($name, [ OrderBy::class ] );
$this->limit = $this->resolver->searchFieldAnnotation($name, [ Limit::class ] );
$this->offset = $this->resolver->searchFieldAnnotation($name, [ Offset::class ] );
$this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Where::class ] ); $this->wheres = $this->resolver->searchFieldAnnotationList($name, [ Where::class ] );
$this->filters = $this->resolver->searchFieldAnnotationList($name, [ Filter::class ] ); $this->filters = $this->resolver->searchFieldAnnotationList($name, [ Filter::class ] );
$this->joins = $this->resolver->searchFieldAnnotationList($name, [ WithJoin::class ] ); $this->joins = $this->resolver->searchFieldAnnotationList($name, [ WithJoin::class ] );
@@ -177,15 +182,32 @@ class RelationBuilder
} }
} }
protected function instanciateEmptyEntity(string $name, Relation $relation) : object protected function applyWithJoin() : void
{
foreach($this->joins as $item) {
$this->repository->withJoin($item->joins);
}
}
protected function applyLimits() : void
{
if ($this->limit) {
$this->repository->limit($this->limit->value);
}
if ($this->offset) {
$this->repository->offset($this->offset->value);
}
}
protected function instanciateEmptyEntity(string $name, Relation|Join $relation) : object
{ {
$class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; $class = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
return new $class(); return new $class();
} }
protected function instanciateEmptyObject(string $name, Relation|Join $relation) : object
protected function instanciateEmptyObject(string $name, Relation $relation) : object
{ {
switch( true ) { switch( true ) {
case $relation->isOneToOne(): case $relation->isOneToOne():
@@ -244,7 +266,7 @@ class RelationBuilder
if ($vars) { if ($vars) {
if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) { if ( [] !== $data = (array_values(array_unique($vars)) !== [ null ] ? $vars : []) ) {
return ( new $entity() )->fromArray($data)->resetVirtualProperties(); return ( new $entity($data) )->resetVirtualProperties();
} }
else { else {
return $this->fieldIsNullable($name) ? null : new $entity(); return $this->fieldIsNullable($name) ? null : new $entity();
@@ -257,40 +279,50 @@ class RelationBuilder
public function oneToOne(string $name, Relation $relation) : Repository public function oneToOne(string $name, Relation $relation) : Repository
{ {
return $this->oneToMany($name, $relation)->limit(1); return $this->oneToMany($name, $relation)->limit($this->limit->value ?? 1);
} }
public function oneToMany(string $name, Relation $relation) : Repository public function oneToMany(string $name, Relation $relation) : Repository
{ {
$baseEntity = $relation->entity ?? $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type; $relation->entity ??= $this->resolver->reflectedClass->getProperties()[$name]->getTypes()[0]->type;
$this->repository = $baseEntity::repository(); $this->repository ??= $relation->entity::repository();
$this->applyWhere(); $this->applyWhere();
$this->applyOrderBy(); $this->applyOrderBy();
$this->applyWithJoin();
$this->applyLimits();
$field = $relation->key; $field = $relation->key;
if ($relation->foreignKey) { if ($relation->foreignKey) {
if ( $relation->generateKey ) { if ( $relation->generateKey ) {
$value = call_user_func_array($relation->generateKey, [ $this->entity ]); $value = call_user_func_array($relation->generateKey, [ $this->entity ]);
} }
else { elseif (isset($this->entity->$field)) {
$value = $this->entity->$field; $value = $this->entity->$field;
} }
$this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $baseEntity::field($relation->foreignKey), $value ); if (isset($value)) {
#$this->repository->where( is_object($relation->foreignKey) ? $relation->foreignKey : $relation->entity::field($relation->foreignKey), $value );
$this->repository->where($relation->foreignKey($this->repository), $value);
}
else {
$this->repository->where(Sql::raw('TRUE'), Sql::raw('FALSE'));
}
} }
return $this->applyFilter($this->repository, $name); return $this->applyFilter($this->repository, $name);
} }
public function manyToMany(string $name, Relation $relation, 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)); extract($this->relationAnnotations($name, $relation));
$this->repository = $relationRelation->entity::repository(); $this->repository ??= $relationRelation->entity::repository();
$bridgeAlias = $relation->bridgeField ?? uniqid("bridge_"); $bridgeAlias = $relation->bridgeField ?? uniqid("bridge_");
@@ -307,6 +339,10 @@ class RelationBuilder
$this->applyOrderBy(); $this->applyOrderBy();
$this->applyWithJoin();
$this->applyLimits();
if ($selectBridgeField && $relation->bridgeField) { if ($selectBridgeField && $relation->bridgeField) {
$this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias); $this->repository->selectEntity($relation->bridge, $bridgeAlias, $bridgeAlias);
} }
@@ -340,7 +376,7 @@ class RelationBuilder
return []; return [];
} }
protected function fieldIsNullable(string $name) : bool protected function fieldIsNullable(string $name) : bool
{ {
return $this->resolver->reflectedClass->getProperties(true)[$name]->allowsNull(); return $this->resolver->reflectedClass->getProperties(true)[$name]->allowsNull();
-1
View File
@@ -27,7 +27,6 @@ class SqliteRepository extends Repository {
return $this->collectionFromQuery(Entity\Sqlite\Column::class)->iterate(fn($e) => $e->tableName = $table); return $this->collectionFromQuery(Entity\Sqlite\Column::class)->iterate(fn($e) => $e->tableName = $table);
} }
protected function serverRequestCountRepository() : Repository protected function serverRequestCountRepository() : Repository
{ {
return new static($this->entityClass, $this->alias, $this->adapter); return new static($this->entityClass, $this->alias, $this->adapter);
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Ulmus\Repository;
enum WithOptionEnum enum WithOptionEnum
{ {
case SkipWhere; case SkipWhere;
case SkipHaving; case SkipHaving;
case SkipFilter; case SkipFilter;
case SkipOrderBy; case SkipOrderBy;
case SkipJoinWhere; case SkipJoinWhere;
@@ -0,0 +1,9 @@
<?php
namespace Ulmus\SearchRequest\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class FieldValueModifier
{
# public function run(mixed $field) : mixed;
}
@@ -0,0 +1,9 @@
<?php
namespace Ulmus\SearchRequest\Attribute;
enum OrderByEnum : string
{
case Ascending = "ASC";
case Descending = "DESC";
}
@@ -3,7 +3,7 @@
namespace Ulmus\SearchRequest\Attribute; namespace Ulmus\SearchRequest\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
abstract class PropertyValueModifier class PropertyValueModifier
{ {
public abstract function run(mixed $value) : mixed; # public function run(mixed $value) : mixed;
} }
@@ -0,0 +1,20 @@
<?php
namespace Ulmus\SearchRequest\Attribute\PropertyValueModifier;
use Ulmus\SearchRequest\Attribute\PropertyValueModifier;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Pad extends PropertyValueModifier
{
public function __construct(
public int $length,
public string $padWith,
public int $type = \STR_PAD_RIGHT,
) {}
public function run(mixed $value) : mixed
{
return str_pad($value, $this->length, $this->padWith, $this->type);
}
}
@@ -13,6 +13,6 @@ class Split extends PropertyValueModifier
public function run(mixed $value) : mixed public function run(mixed $value) : mixed
{ {
return explode($this->delimiter, $value); return is_array($value) ? $value : explode($this->delimiter, $value);
} }
} }
@@ -0,0 +1,18 @@
<?php
namespace Ulmus\SearchRequest\Attribute\PropertyValueModifier;
use Ulmus\SearchRequest\Attribute\PropertyValueModifier;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Trim extends PropertyValueModifier
{
public function __construct(
public string $characters = ' \t',
) {}
public function run(mixed $value) : mixed
{
return trim($value, $this->characters);
}
}
@@ -16,5 +16,6 @@ class SearchOrderBy extends SearchParameter
public null|string|\Stringable|array $field = null, public null|string|\Stringable|array $field = null,
public null|SearchMethodEnum $order = null, public null|SearchMethodEnum $order = null,
public string $description = "", public string $description = "",
public PropertyValueSource|array $source = PropertyValueSource::QueryParams,
) {} ) {}
} }
@@ -5,7 +5,7 @@ namespace Ulmus\SearchRequest\Attribute;
use Ulmus\SearchRequest\SearchMethodEnum; use Ulmus\SearchRequest\SearchMethodEnum;
#[\Attribute(\Attribute::TARGET_PROPERTY)] #[\Attribute(\Attribute::TARGET_PROPERTY)]
abstract class SearchParameter { class SearchParameter {
public PropertyValueSource|array $source = PropertyValueSource::QueryParams; public PropertyValueSource|array $source = PropertyValueSource::QueryParams;
@@ -20,6 +20,6 @@ abstract class SearchParameter {
public function getParameters() : array public function getParameters() : array
{ {
return array_filter((array) $this->parameter); return array_filter((array) ( $this->parameter ?? [] ));
} }
} }
@@ -0,0 +1,107 @@
<?php
namespace Ulmus\SearchRequest;
use Psr\Http\Message\ServerRequestInterface;
use Ulmus\Repository;
trait DeprecatedSearchRequestFormatTrait
{
protected array $wheres = [];
protected array $wheresConditions = [];
protected array $wheresOperators = [];
protected array $likes = [];
protected array $likesOperators = [];
protected array $likesConditions = [];
protected array $groups = [];
protected array $orders = [];
/* [[ Boilerplates which are ready to be copied */
public function wheres() : iterable
{
return array_filter($this->wheres + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function wheresConditions() : iterable
{
return array_filter($this->wheresConditions + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function wheresOperators() : iterable
{
return array_filter($this->wheresOperators + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function likes(): iterable
{
return array_filter($this->likes + [
], fn($i) => ! is_null($i) ) + [];
}
public function likesConditions() : iterable
{
return array_filter($this->likesConditions + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function likesOperators() : iterable
{
return array_filter($this->likesOperators + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function groups(): iterable
{
return array_filter($this->groups + [
], fn($e) => ! is_null($e) && $e !== "" );
}
public function orders(): iterable
{
return array_filter($this->orders + [
], fn($e) => ! is_null($e) && $e !== "" );
}
public function applyCount(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->count = $this->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->groups($this->groups())
->count();
return $this;
}
public function apply(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->orders($this->orders())
->groups($this->groups())
->offset($this->offset())
->limit($this->limit());
return $this;
}
/* ]] */
}
@@ -0,0 +1,48 @@
<?php
namespace Ulmus\SearchRequest;
class ParsedSearchParameter
{
public string|\Stringable $operator;
public string|\Stringable $condition;
public function __construct(
public readonly SearchMethodEnum $searchMethod,
public readonly string|\Stringable $field,
public readonly mixed $value,
) {}
public function value() : mixed{
return match ($this->searchMethod) {
SearchMethodEnum::Like => "%$this->value%",
SearchMethodEnum::LikeLeft => "%$this->value",
SearchMethodEnum::LikeRight => "$this->value%",
default => $this->value,
};
}
public function operator() : string {
if (isset($this->operator)) {
return $this->operator;
}
return match ($this->searchMethod) {
SearchMethodEnum::Like => SearchRequestInterface::SEARCH_REQUEST_OPERATOR_LIKE,
default => SearchRequestInterface::SEARCH_REQUEST_OPERATOR_EQUAL,
};
}
public function condition() : string {
if (isset($this->condition)) {
return $this->condition;
}
return match ($this->searchMethod) {
SearchMethodEnum::Like => SearchRequestInterface::SEARCH_REQUEST_CONDITION_OR,
default => SearchRequestInterface::SEARCH_REQUEST_CONDITION_AND,
};
}
}
+2
View File
@@ -9,4 +9,6 @@ enum SearchMethodEnum
case Like; case Like;
case LikeLeft; case LikeLeft;
case LikeRight; case LikeRight;
case OrderBy;
case GroupBy;
} }
+79 -4
View File
@@ -9,33 +9,108 @@ class SearchRequest implements SearchRequestInterface
{ {
use SearchRequestPaginationTrait, SearchRequestFromRequestTrait; use SearchRequestPaginationTrait, SearchRequestFromRequestTrait;
public function filter(Repository $repository) : Repository protected array $wheres = [];
{
return $repository;
}
protected array $wheresConditions = [];
protected array $wheresOperators = [];
protected array $likes = [];
protected array $likesOperators = [];
protected array $likesConditions = [];
protected array $groups = [];
protected array $orders = [];
/* [[ Boilerplates which are ready to be copied */
public function wheres() : iterable public function wheres() : iterable
{ {
return array_filter($this->wheres + [ return array_filter($this->wheres + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function wheresConditions() : iterable
{
return array_filter($this->wheresConditions + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function wheresOperators() : iterable
{
return array_filter($this->wheresOperators + [
], fn($i) => ! is_null($i) ) + [ ]; ], fn($i) => ! is_null($i) ) + [ ];
} }
public function likes(): iterable public function likes(): iterable
{ {
return array_filter($this->likes + [ return array_filter($this->likes + [
], fn($i) => ! is_null($i) ) + []; ], fn($i) => ! is_null($i) ) + [];
} }
public function likesConditions() : iterable
{
return array_filter($this->likesConditions + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function likesOperators() : iterable
{
return array_filter($this->likesOperators + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function groups(): iterable public function groups(): iterable
{ {
return array_filter($this->groups + [ return array_filter($this->groups + [
], fn($e) => ! is_null($e) && $e !== "" ); ], fn($e) => ! is_null($e) && $e !== "" );
} }
public function orders(): iterable public function orders(): iterable
{ {
return array_filter($this->orders + [ return array_filter($this->orders + [
], fn($e) => ! is_null($e) && $e !== "" ); ], fn($e) => ! is_null($e) && $e !== "" );
} }
/* ]] */
public function filter(Repository $repository) : Repository
{
return $repository;
}
public function applyCount(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->count = $this->fromParsedParameters($repository)
->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->groups($this->groups())
->count();
return $this;
}
public function apply(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->fromParsedParameters($repository)
->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->orders($this->orders())
->groups($this->groups())
->offset($this->offset())
->limit($this->limit());
return $this;
}
} }
@@ -2,6 +2,7 @@
namespace Ulmus\SearchRequest; namespace Ulmus\SearchRequest;
use Notes\Common\ReflectedClass;
use Notes\ObjectReflection; use Notes\ObjectReflection;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@@ -14,19 +15,73 @@ use Ulmus\SearchRequest\Attribute\{PropertyValueSource,
SearchOrderBy, SearchOrderBy,
SearchGroupBy, SearchGroupBy,
SearchRequestParameter}; SearchRequestParameter};
use Ulmus\Repository;
trait SearchRequestFromRequestTrait trait SearchRequestFromRequestTrait
{ {
protected array $wheres = []; protected ReflectedClass $objectReflection;
protected array $likes = []; protected array $parsedParameters = [];
protected array $groups = []; public function toArray() : array
protected array $orders = [];
public function fromRequest(ServerRequestInterface $request)
{ {
return json_decode(json_encode($this), true);
}
public function fromParsedParameters(Repository $repository) : SearchRequestInterface
{
foreach($this->parsedParameters as $property => $param) {
switch($param->searchMethod) {
case SearchMethodEnum::Like:
case SearchMethodEnum::LikeLeft:
case SearchMethodEnum::LikeRight:
$repository->like($param->field, $param->value());
break;
case SearchMethodEnum::Where:
$repository->where($param->field, $param->value(), $param->operator(), $param->condition());
break;
case SearchMethodEnum::OrderBy:
$value = $param->value();
$repository->orderBy($param->field, $value instanceof \BackedEnum ? $value->value : $value);
break;
case SearchMethodEnum::GroupBy:
$repository->groupBy($param->field);
break;
}
}
return $this;
}
public function fromArray(array|\ArrayAccess $data) : static
{
$this->objectReflection = ObjectReflection::fromClass(static::class)->reflectClass();
$this->page = $data['page'] ?? 1;
$this->applyValues(
fn($propertyName, $attribute) => $data[$propertyName] ?? null
);
$this->applyOperators(
fn($propertyName) => $data["$propertyName:operator"] ?? null
);
$this->applyConditions(
fn($propertyName) => $data["$propertyName:condition"] ?? null
);
return $this;
}
public function fromRequest(ServerRequestInterface $request) : static
{
$this->objectReflection = ObjectReflection::fromClass(static::class)->reflectClass();
if (method_exists($this, 'prepare')) { if (method_exists($this, 'prepare')) {
$this->prepare($request); $this->prepare($request);
} }
@@ -35,32 +90,81 @@ trait SearchRequestFromRequestTrait
$this->page = $queryParams->offsetExists('page') ? $queryParams['page'] : 1; $this->page = $queryParams->offsetExists('page') ? $queryParams['page'] : 1;
$classReflection = ObjectReflection::fromClass(static::class)->reflectClass(); $this->applyValues(
fn($propertyName, $attribute) =>$this->getValueFromSource($request, $propertyName, $attribute)
);
foreach($classReflection->getProperties() as $propertyName => $property) { $operators = $request->getAttribute(SearchRequestInterface::SEARCH_REQUEST_OPERATORS);
$attributeList = $property->getAttributes(Attribute\SearchParameter::class);
if ($attributeList) { if ($operators) {
$attribute = $attributeList[0]->object; $this->applyOperators(
fn($propertyName) => $operators[$propertyName] ?? null
);
}
$fieldName = $attribute->field ?? $propertyName; $conditions = $request->getAttribute(SearchRequestInterface::SEARCH_REQUEST_CONDITIONS);
# Field could be defined for another entity class if ($conditions) {
if (is_array($fieldName)) { $this->applyConditions(
$field = \Ulmus\Attribute\Attribute::handleArrayField($fieldName, false); fn($propertyName) => $conditions[$propertyName] ?? null
} );
# Default class using it, if SearchRequestParameter is defined }
elseif ($classAttributes = $classReflection->getAttributes(SearchRequestParameter::class)) {
$searchRequestAttribute = $classAttributes[0]->object; return $this;
$className = $searchRequestAttribute->class; }
$field = $className::field($fieldName, $searchRequestAttribute->alias);
} protected function applyConditions(\Closure $getCondition) : void
# Untouched string from the attribute {
else { foreach($this->objectReflection->getProperties() as $propertyName => $property) {
$field = $fieldName; if (! isset($this->parsedParameters[$propertyName])) {
continue;
}
$attribute = $property->getAttribute(Attribute\SearchParameter::class)->object ?? false;
if ( $condition = $getCondition($propertyName) ) {
match(true) {
$attribute instanceof SearchManual => throw new \LogicException("Impossible to add a condition to a manually filtered elements."),
$attribute instanceof SearchGroupBy => throw new \LogicException("Impossible to add a condition to a group by field."),
$attribute instanceof SearchOrderBy => throw new \LogicException("Impossible to add a condition to a order by field."),
default => $this->parsedParameters[$propertyName]->condition = $condition,
};
}
}
}
protected function applyOperators(\Closure $getOperator) : void
{
foreach($this->objectReflection->getProperties() as $propertyName => $property) {
if (! isset($this->parsedParameters[$propertyName])) {
continue;
}
$attribute = $property->getAttribute(Attribute\SearchParameter::class)->object ?? false;
if ($attribute) {
if ($operator = $getOperator($propertyName)) {
match (true) {
$attribute instanceof SearchManual => throw new \LogicException("Impossible to add an operator to a manually filtered elements."),
$attribute instanceof SearchGroupBy => throw new \LogicException("Impossible to add an operator to a group by field."),
$attribute instanceof SearchOrderBy => throw new \LogicException("Impossible to add an operator to a order by field."),
default => $this->parsedParameters[$propertyName]->operator = $operator,
};
} }
}
}
}
$value = $this->getValueFromSource($request, $propertyName, $attribute); protected function applyValues(\Closure $getValue) : void
{
foreach($this->objectReflection->getProperties() as $propertyName => $property) {
$attribute = $property->getAttribute(Attribute\SearchParameter::class)->object ?? false;
if ($attribute) {
$field = $this->getEntityField($attribute->field ?? $propertyName);
$field = $this->transformField($property->getAttributes(Attribute\FieldValueModifier::class), $field);
$value = $getValue($propertyName, $attribute);
if ($value !== null) { if ($value !== null) {
$value = $this->transformValue($property->getAttributes(Attribute\PropertyValueModifier::class), $value); $value = $this->transformValue($property->getAttributes(Attribute\PropertyValueModifier::class), $value);
@@ -71,51 +175,79 @@ trait SearchRequestFromRequestTrait
$this->parseAttributeGroupBy($attribute, $field, $propertyName); $this->parseAttributeGroupBy($attribute, $field, $propertyName);
break; break;
case $attribute instanceof SearchOrderBy:
if ($value !== null) {
$this->$propertyName = $this->parseValue($property, $value);
}
$this->parseAttributeOrderBy($attribute, $field, $propertyName);
break;
case $attribute instanceof SearchWhere: case $attribute instanceof SearchWhere:
case $attribute instanceof SearchLike: case $attribute instanceof SearchLike:
case $attribute instanceof SearchManual: case $attribute instanceof SearchManual:
if ($attribute->toggle) { if ($attribute->toggle) {
$this->$propertyName = !empty($value); $this->$propertyName = !empty($value);
} elseif ($value !== null) { }
foreach ($property->getTypes() as $type) { elseif ($value !== null) {
$enum = $type->type; $this->$propertyName = $this->parseValue($property, $value);
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 || $value instanceof \Stringable ) {
$this->$propertyName = $value;
}
}
} }
$this->parseAttributeMethod($attribute, $field, $propertyName); $this->parseAttributeMethod($attribute, $field, $propertyName);
break; break;
case $attribute instanceof SearchOrderBy:
if ($value !== null) {
$this->$propertyName = $value;
}
$this->parseAttributeOrderBy($attribute, $field, $propertyName);
break;
} }
} }
} }
}
return $this; protected function parseValue(\Notes\Common\ReflectedProperty $property, mixed $value) : mixed
{
foreach ($property->getTypes() as $type) {
$enum = $type->type;
if (enum_exists($enum)) {
try {
return $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 || $value instanceof \Stringable ) {
return $value;
}
}
}
protected function getEntityField(array|string $fieldName) : string|\Stringable
{
# Field could be defined for another entity class
if (is_array($fieldName)) {
$field = \Ulmus\Attribute\Attribute::handleArrayField($fieldName, false);
}
# Default class using it, if SearchRequestParameter is defined
elseif ($classAttributes = $this->objectReflection->getAttributes(SearchRequestParameter::class)) {
$searchRequestAttribute = $classAttributes[0]->object;
$className = $searchRequestAttribute->class;
$field = $className::field($fieldName, $searchRequestAttribute->alias);
}
return $field ?? $fieldName;
} }
protected function getValueFromSource(RequestInterface $request, string $propertyName, SearchParameter $attribute) : mixed protected function getValueFromSource(RequestInterface $request, string $propertyName, SearchParameter $attribute) : mixed
{ {
$classAttributes = $this->objectReflection->getAttributes(SearchRequestParameter::class);
$searchRequestAttribute = $classAttributes[0]->object;
$className = $searchRequestAttribute->class;
if ( $override = $request->getAttribute(sprintf("searchRequest.%s:%s", $className, $propertyName)) ) {
return $override;
}
$queryParamName = $attribute->getParameters() ?: [ $propertyName ]; $queryParamName = $attribute->getParameters() ?: [ $propertyName ];
foreach($attribute->getSources() as $source) { foreach($attribute->getSources() as $source) {
@@ -147,7 +279,16 @@ trait SearchRequestFromRequestTrait
return null; return null;
} }
protected function transformValue(array $valueModifierAttributes, mixed $value, ) : mixed protected function transformField(array $fieldModifierAttributes, mixed $field) : mixed
{
foreach($fieldModifierAttributes as $transform) {
$field = $transform->object->run($field);
}
return $field;
}
protected function transformValue(array $valueModifierAttributes, mixed $value) : mixed
{ {
foreach($valueModifierAttributes as $transform) { foreach($valueModifierAttributes as $transform) {
$value = $transform->object->run($value); $value = $transform->object->run($value);
@@ -156,42 +297,43 @@ trait SearchRequestFromRequestTrait
return $value; return $value;
} }
protected function parseAttributeMethod(object $attribute, string $field, string $propertyName,) : void protected function parseAttributeMethod(object $attribute, string|\Stringable $field, string $propertyName) : void
{ {
if (! isset($this->$propertyName)) { if (! isset($this->$propertyName)) {
return; return;
} }
switch ($attribute->method) { switch ($attribute->method) {
case SearchMethodEnum::Where:
$this->wheres[$field] = $this->$propertyName;
break;
case SearchMethodEnum::Like: case SearchMethodEnum::Like:
$this->likes[$field] = "%{$this->$propertyName}%";
break;
case SearchMethodEnum::LikeLeft: case SearchMethodEnum::LikeLeft:
$this->likes[$field] = "%{$this->$propertyName}";
break;
case SearchMethodEnum::LikeRight: case SearchMethodEnum::LikeRight:
$this->likes[$field] = "{$this->$propertyName}%"; case SearchMethodEnum::Where:
break; $this->parsedParameters[$propertyName] = new ParsedSearchParameter(
searchMethod: $attribute->method,
field: $field,
value: $this->$propertyName,
);
} }
} }
protected function parseAttributeOrderBy(object $attribute, string $field, mixed $propertyName,) : void protected function parseAttributeOrderBy(object $attribute, string $field, mixed $propertyName) : void
{ {
if ( ! empty($this->$propertyName) ) { if ( ! empty($this->$propertyName) ) {
$this->orders[$field] = $this->$propertyName; $this->parsedParameters[$propertyName] = new ParsedSearchParameter(
searchMethod: SearchMethodEnum::OrderBy,
field: $field,
value: $this->$propertyName,
);
} }
} }
protected function parseAttributeGroupBy(object $attribute, string $field, mixed $propertyName,) : void protected function parseAttributeGroupBy(object $attribute, string $field, mixed $propertyName) : void
{ {
if (! empty($this->$propertyName)) { if (! empty($this->$propertyName)) {
$this->groups[] = $field; $this->parsedParameters[$propertyName] = new ParsedSearchParameter(
searchMethod: SearchMethodEnum::GroupBy,
field: $field,
value: null,
);
} }
} }
} }
+27 -2
View File
@@ -5,11 +5,32 @@ namespace Ulmus\SearchRequest;
use Ulmus\Repository; use Ulmus\Repository;
interface SearchRequestInterface { interface SearchRequestInterface {
public const SEARCH_REQUEST_OPERATORS = "searchRequest:operators";
public const SEARCH_REQUEST_OPERATOR_LIKE = "LIKE";
public const SEARCH_REQUEST_OPERATOR_EQUAL = "=";
public const SEARCH_REQUEST_OPERATOR_NOT_EQUAL = "!=";
public const SEARCH_REQUEST_OPERATOR_GREATER_THAN = ">";
public const SEARCH_REQUEST_OPERATOR_GREATER_OR_EQUAL_THAN = ">=";
public const SEARCH_REQUEST_OPERATOR_SMALLER_THAN = "<";
public const SEARCH_REQUEST_OPERATOR_SMALLER_OR_EQUAL_THAN = "<=";
public const SEARCH_REQUEST_CONDITIONS = "searchRequest:conditions";
public const SEARCH_REQUEST_CONDITION_AND = "AND";
public const SEARCH_REQUEST_CONDITION_OR= "OR";
public function filter(Repository $repository) : Repository; public function filter(Repository $repository) : Repository;
public function wheres() : iterable; public function wheres() : iterable;
public function wheresConditions() : iterable;
public function wheresOperators() : iterable;
public function likes() : iterable; public function likes() : iterable;
public function likesConditions() : iterable;
public function likesOperators() : iterable;
public function orders() : iterable; public function orders() : iterable;
@@ -18,4 +39,8 @@ interface SearchRequestInterface {
public function limit() : int; public function limit() : int;
public function offset() : int; public function offset() : int;
public function applyCount(Repository\RepositoryInterface $repository) : SearchRequestInterface;
public function apply(Repository\RepositoryInterface $repository) : SearchRequestInterface;
} }
@@ -2,14 +2,20 @@
namespace Ulmus\SearchRequest; namespace Ulmus\SearchRequest;
use Ulmus\Repository;
use Ulmus\SearchRequest\Attribute\SearchManual;
trait SearchRequestPaginationTrait { trait SearchRequestPaginationTrait {
use DeprecatedSearchRequestFormatTrait;
public int $count = 0; public int $count = 0;
#[SearchManual]
public int $page = 1; public int $page = 1;
public int $pageCount = 0; public int $pageCount = 0;
#[SearchManual]
public int $limit = 25; public int $limit = 25;
public bool $skipCount = false; public bool $skipCount = false;
@@ -72,4 +78,61 @@ trait SearchRequestPaginationTrait {
return $this; return $this;
} }
public function wheresConditions() : iterable
{
return array_filter(($this->wheresConditions ?? []) + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function wheresOperators() : iterable
{
return array_filter(($this->wheresOperators ?? []) + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function likesConditions() : iterable
{
return array_filter(($this->likesConditions ?? []) + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function likesOperators() : iterable
{
return array_filter(($this->likesOperators ?? []) + [
], fn($i) => ! is_null($i) ) + [ ];
}
public function filter(Repository $repository) : Repository
{
return $repository;
}
public function applyCount(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->count = $this->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->groups($this->groups())
->count();
return $this;
}
public function apply(Repository\RepositoryInterface $repository): SearchRequestInterface
{
$this->filter($repository)
->wheres($this->wheres(), $this->wheresOperators(), $this->wheresConditions())
->likes($this->likes(), $this->likesConditions())
->orders($this->orders())
->groups($this->groups())
->offset($this->offset())
->limit($this->limit());
return $this;
}
} }
+1 -1
View File
@@ -108,7 +108,7 @@ abstract class Ulmus
return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass, static::$cache); return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass, static::$cache);
} }
public static function repository(string $entityClass, string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository public static function repository(string $entityClass, string $alias = Repository::DEFAULT_ALIAS, ?ConnectionAdapter $adapter = null) : Repository
{ {
$adapter ??= $entityClass::resolveEntity()->sqlAdapter(); $adapter ??= $entityClass::resolveEntity()->sqlAdapter();
$cls = $adapter->adapter()->repositoryClass(); $cls = $adapter->adapter()->repositoryClass();