Compare commits
57
Commits
70c6cde603
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9507a1e596 | ||
|
|
f197443871 | ||
|
|
f6bbdda137 | ||
|
|
eb505f4772 | ||
|
|
25ab845886 | ||
|
|
c417b00ed4 | ||
|
|
caf5bd9664 | ||
|
|
9062a73ee6 | ||
|
|
e5a842a812 | ||
|
|
13b7cf06f1 | ||
|
|
709ed63323 | ||
|
|
5d32d9f635 | ||
|
|
1e4a528d39 | ||
|
|
2d86adc3e1 | ||
|
|
2a03789e7c | ||
|
|
1af0063203 | ||
|
|
7b35de3258 | ||
|
|
f938e18572 | ||
|
|
b394b9348f | ||
|
|
009e30eb7e | ||
|
|
fd5d31fa17 | ||
|
|
b0b3aca6d9 | ||
|
|
36162c6486 | ||
|
|
11e436236c | ||
|
|
a9b9c2cce9 | ||
|
|
cb125545c7 | ||
|
|
2271b0b034 | ||
|
|
2e45eba520 | ||
|
|
033a183269 | ||
|
|
0192975f10 | ||
|
|
b33c67b594 | ||
|
|
2f482324b2 | ||
|
|
a5d65025f8 | ||
|
|
b53327a6ed | ||
|
|
fb644b0b0a | ||
|
|
c719e4a6d1 | ||
|
|
fb4985160a | ||
|
|
c54de30069 | ||
|
|
8fff21ecb0 | ||
|
|
cc6048d4ee | ||
|
|
761e0401b0 | ||
|
|
1ec582d1ac | ||
|
|
9556dea849 | ||
|
|
bfddf84564 | ||
|
|
e0ec140661 | ||
|
|
0fd6889c6f | ||
|
|
bd0d928f35 | ||
|
|
5a5d326d70 | ||
|
|
b5e96eb7e0 | ||
|
|
7c589ab7b4 | ||
|
|
589524ca3a | ||
|
|
1fac12c928 | ||
|
|
9eed2fe1a8 | ||
|
|
b601939459 | ||
|
|
e3314f40ac | ||
|
|
4ad44f73d7 | ||
|
|
68a6636c06 |
No files matched your search
+3
-3
@@ -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": {
|
||||||
|
|||||||
@@ -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*/
|
||||||
|
|||||||
@@ -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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+28
-5
@@ -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',
|
||||||
@@ -51,7 +56,9 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
?string $server = null,
|
?string $server = null,
|
||||||
?string $database = null,
|
?string $database = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
?string $username = null,
|
?string $username = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
?string $password = null,
|
?string $password = null,
|
||||||
?int $port = null
|
?int $port = null
|
||||||
) {
|
) {
|
||||||
@@ -76,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);
|
||||||
@@ -204,6 +211,13 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
@@ -233,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* */
|
||||||
|
}
|
||||||
+30
-4
@@ -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',
|
||||||
@@ -38,7 +39,9 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
?string $hostname = null,
|
?string $hostname = null,
|
||||||
?string $database = null,
|
?string $database = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
?string $username = null,
|
?string $username = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
?string $password = null,
|
?string $password = null,
|
||||||
?int $port = null,
|
?int $port = null,
|
||||||
?string $charset = null
|
?string $charset = null
|
||||||
@@ -68,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);
|
||||||
@@ -150,6 +153,7 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface {
|
|||||||
public static function escapeIdentifier(string $segment, int $type) : string
|
public static function escapeIdentifier(string $segment, int $type) : string
|
||||||
{
|
{
|
||||||
switch($type) {
|
switch($type) {
|
||||||
|
default:
|
||||||
case static::IDENTIFIER_DATABASE:
|
case static::IDENTIFIER_DATABASE:
|
||||||
case static::IDENTIFIER_TABLE:
|
case static::IDENTIFIER_TABLE:
|
||||||
case static::IDENTIFIER_FIELD:
|
case static::IDENTIFIER_FIELD:
|
||||||
@@ -177,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,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
+72
-38
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
namespace Ulmus\Adapter;
|
namespace Ulmus\Adapter;
|
||||||
|
|
||||||
use Ulmus\Common\PdoObject;
|
use Collator;
|
||||||
|
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'
|
'default', 'primary_key', 'auto_increment', 'collate nocase', 'collate binary', 'collate rtrim',
|
||||||
];
|
];
|
||||||
|
|
||||||
const DSN_PREFIX = "sqlite";
|
const DSN_PREFIX = "sqlite";
|
||||||
@@ -24,19 +24,20 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->exportFunctions($pdo);
|
$this->exportFunctions($pdo);
|
||||||
|
$this->exportCollations($pdo);
|
||||||
$this->registerPragma($pdo, $this->pragmaBegin);
|
$this->registerPragma($pdo, $this->pragmaBegin);
|
||||||
}
|
}
|
||||||
catch(\PDOException $ex){
|
catch(\PDOException $ex){
|
||||||
@@ -101,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) {
|
||||||
@@ -133,10 +135,6 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($type, [ 'JSON', 'TEXT', 'BLOB', 'GEOMETRY' ])) {
|
|
||||||
$field->default = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $typeOnly ? $type : $type . ( $length ? "($length" . ")" : "" );
|
return $typeOnly ? $type : $type . ( $length ? "($length" . ")" : "" );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,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;
|
||||||
}
|
}
|
||||||
@@ -191,12 +189,43 @@ 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('d'), 1);
|
$pdo->createFunction('day', fn($date) => ( new \DateTime($date) )->format('j'), 1);
|
||||||
$pdo->sqliteCreateFunction('month', fn($date) => ( new \DateTime($date) )->format('m'), 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 static function registerPragma(PdoObject $pdo, array $pragmaList) : void
|
public function exportCollations(\PDO $pdo) : void
|
||||||
|
{
|
||||||
|
|
||||||
|
if ( class_exists('Locale') ) {
|
||||||
|
# @TODO debug this, case-sensitivity not working properly !!
|
||||||
|
$collator = new Collator(\Locale::getDefault());
|
||||||
|
$collator->setStrength(Collator::TERTIARY);
|
||||||
|
$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
|
||||||
|
|
||||||
|
$pdo->createCollation('locale_cmp', fn($e1, $e2) => $collator->compare($e1, $e2));
|
||||||
|
$pdo->createCollation('locale_cmp_desc', fn($e1, $e2) => $collator->compare($e2, $e1));
|
||||||
|
|
||||||
|
$collatorCi = new Collator(\Locale::getDefault());
|
||||||
|
$collatorCi->setStrength(Collator::SECONDARY);
|
||||||
|
$collatorCi->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
|
||||||
|
|
||||||
|
$pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $collatorCi->compare($e1, $e2));
|
||||||
|
$pdo->createCollation('locale_cmp_ci_desc', fn($e1, $e2) => $collatorCi->compare($e2, $e1));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$comp = fn(string $e1, string $e2) => \iconv('UTF-8', 'ASCII//TRANSLIT', $e1) <=> \iconv('UTF-8', 'ASCII//TRANSLIT', $e2);
|
||||||
|
$pdo->createCollation('locale_cmp', fn($e1, $e2) => $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));
|
||||||
|
$pdo->createCollation('locale_cmp_ci', fn($e1, $e2) => $compCi);
|
||||||
|
$pdo->createCollation('locale_cmp_ci_desc', fn($e2, $e1) => $compCi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function registerPragma(\PDO $pdo, array $pragmaList) : void
|
||||||
{
|
{
|
||||||
$builder = new QueryBuilder\Sql\SqliteQueryBuilder();
|
$builder = new QueryBuilder\Sql\SqliteQueryBuilder();
|
||||||
|
|
||||||
@@ -224,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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
namespace Ulmus\Adapter;
|
namespace Ulmus\Adapter;
|
||||||
|
|
||||||
use Ulmus\Migration\FieldDefinition;
|
use Ulmus\Migration\FieldDefinition;
|
||||||
|
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,
|
||||||
@@ -42,16 +43,18 @@ class SqlFieldMapper
|
|||||||
switch($type) {
|
switch($type) {
|
||||||
case "bool":
|
case "bool":
|
||||||
$this->type = "TINYINT";
|
$this->type = "TINYINT";
|
||||||
$length = 1;
|
$this->length = 1;
|
||||||
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":
|
||||||
if ($length && $length <= 255) {
|
if ($length && $length <= 255) {
|
||||||
$this->type = "VARCHAR";
|
$this->type = "VARCHAR";
|
||||||
|
$this->length = $length;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
elseif (! $length || ( $length <= 65535 ) ) {
|
elseif (! $length || ( $length <= 65535 ) ) {
|
||||||
@@ -74,11 +77,13 @@ class SqlFieldMapper
|
|||||||
|
|
||||||
case "float":
|
case "float":
|
||||||
$this->type = "DOUBLE";
|
$this->type = "DOUBLE";
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$this->type = strtoupper($type);
|
if ($length) {
|
||||||
break;
|
$this->length = $length;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->type ??= strtoupper($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->postProcess();
|
$this->postProcess();
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ use Ulmus\Repository;
|
|||||||
|
|
||||||
class Attribute
|
class Attribute
|
||||||
{
|
{
|
||||||
public static function handleArrayField(null|\Stringable|string|array $field, null|string|bool $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
|
public static function handleArrayField(null|\Stringable|string|array $field, null|string|false $alias = Repository::DEFAULT_ALIAS, string $separator = ', ') : mixed
|
||||||
{
|
{
|
||||||
if ( is_array($field) ) {
|
if ( 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;
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ namespace Ulmus\Attribute\Obj;
|
|||||||
|
|
||||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||||
class Collation {
|
class Collation {
|
||||||
|
public function __construct(
|
||||||
|
public string $name = ""
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Obj;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||||
|
class JsonSerialize {
|
||||||
|
public function __construct(
|
||||||
|
public bool $includeRelations = true,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class Collation {
|
||||||
|
public function __construct(
|
||||||
|
public string $name = ""
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -16,6 +16,6 @@ class Id extends \Ulmus\Attribute\Property\Field {
|
|||||||
],
|
],
|
||||||
public bool $nullable = false,
|
public bool $nullable = false,
|
||||||
public mixed $default = null,
|
public mixed $default = null,
|
||||||
public bool $readonly = false,
|
public bool $readonly = true,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property\Field;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class Select {
|
||||||
|
public function __construct(
|
||||||
|
public string $sql,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -6,6 +6,5 @@ namespace Ulmus\Attribute\Property;
|
|||||||
class Filter {
|
class Filter {
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $method = ""
|
public string $method = ""
|
||||||
)
|
) {}
|
||||||
{}
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class JsonSerialize {
|
||||||
|
public function __construct(
|
||||||
|
public bool $ignoreField = false,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class Limit {
|
||||||
|
public function __construct(
|
||||||
|
public int $value,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class Offset {
|
||||||
|
public function __construct(
|
||||||
|
public int $value,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ class OrWhere extends Where {
|
|||||||
public string $operator = Query\Where::OPERATOR_EQUAL,
|
public string $operator = Query\Where::OPERATOR_EQUAL,
|
||||||
public string $condition = Query\Where::CONDITION_OR,
|
public string $condition = Query\Where::CONDITION_OR,
|
||||||
public string|\Stringable|array|null $fieldValue = null,
|
public string|\Stringable|array|null $fieldValue = null,
|
||||||
public null|array $generateValue = null,
|
public null|array|\Closure $generateValue = null,
|
||||||
) {
|
) {
|
||||||
parent::__construct($field, $value, $operator, $condition, $fieldValue, $generateValue);
|
parent::__construct($field, $value, $operator, $condition, $fieldValue, $generateValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
use Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class ManyToMany extends Relation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public \Stringable|string|array $key = "",
|
||||||
|
public null|\Closure|array $generateKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignField = null,
|
||||||
|
public array $foreignKeys = [],
|
||||||
|
public null|string $bridge = null,
|
||||||
|
public null|\Stringable|string|array $bridgeKey = null,
|
||||||
|
public null|\Stringable|string|array $bridgeField = null,
|
||||||
|
public null|\Stringable|string|array $bridgeForeignKey = null,
|
||||||
|
public null|\Stringable|string|array $field = null,
|
||||||
|
public null|string $entity = null,
|
||||||
|
public null|string $join = null,
|
||||||
|
public null|string $function = null,
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
RelationTypeEnum::manyToMany,
|
||||||
|
$this->key,
|
||||||
|
$this->generateKey,
|
||||||
|
$this->foreignKey,
|
||||||
|
$this->foreignField,
|
||||||
|
$this->foreignKeys,
|
||||||
|
$this->bridge,
|
||||||
|
$this->bridgeKey,
|
||||||
|
$this->bridgeField,
|
||||||
|
$this->bridgeForeignKey,
|
||||||
|
$this->field,
|
||||||
|
$this->entity,
|
||||||
|
$this->join,
|
||||||
|
$this->function,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
use Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class OneToMany extends Relation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public \Stringable|string|array $key = "",
|
||||||
|
public null|\Closure|array $generateKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignField = null,
|
||||||
|
public array $foreignKeys = [],
|
||||||
|
public null|string $bridge = null,
|
||||||
|
public null|\Stringable|string|array $bridgeKey = null,
|
||||||
|
public null|\Stringable|string|array $bridgeField = null,
|
||||||
|
public null|\Stringable|string|array $bridgeForeignKey = null,
|
||||||
|
public null|\Stringable|string|array $field = null,
|
||||||
|
public null|string $entity = null,
|
||||||
|
public null|string $join = null,
|
||||||
|
public null|string $function = null,
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
RelationTypeEnum::oneToMany,
|
||||||
|
$this->key,
|
||||||
|
$this->generateKey,
|
||||||
|
$this->foreignKey,
|
||||||
|
$this->foreignField,
|
||||||
|
$this->foreignKeys,
|
||||||
|
$this->bridge,
|
||||||
|
$this->bridgeKey,
|
||||||
|
$this->bridgeField,
|
||||||
|
$this->bridgeForeignKey,
|
||||||
|
$this->field,
|
||||||
|
$this->entity,
|
||||||
|
$this->join,
|
||||||
|
$this->function,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
use Ulmus\Attribute\Property\Relation;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||||
|
class OneToOne extends Relation
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public \Stringable|string|array $key = "",
|
||||||
|
public null|\Closure|array $generateKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignKey = null,
|
||||||
|
public null|\Stringable|string|array $foreignField = null,
|
||||||
|
public array $foreignKeys = [],
|
||||||
|
public null|string $bridge = null,
|
||||||
|
public null|\Stringable|string|array $bridgeKey = null,
|
||||||
|
public null|\Stringable|string|array $bridgeField = null,
|
||||||
|
public null|\Stringable|string|array $bridgeForeignKey = null,
|
||||||
|
public null|\Stringable|string|array $field = null,
|
||||||
|
public null|string $entity = null,
|
||||||
|
public null|string $join = null,
|
||||||
|
public null|string $function = null,
|
||||||
|
) {
|
||||||
|
parent::__construct(
|
||||||
|
RelationTypeEnum::oneToOne,
|
||||||
|
$this->key,
|
||||||
|
$this->generateKey,
|
||||||
|
$this->foreignKey,
|
||||||
|
$this->foreignField,
|
||||||
|
$this->foreignKeys,
|
||||||
|
$this->bridge,
|
||||||
|
$this->bridgeKey,
|
||||||
|
$this->bridgeField,
|
||||||
|
$this->bridgeForeignKey,
|
||||||
|
$this->field,
|
||||||
|
$this->entity,
|
||||||
|
$this->join,
|
||||||
|
$this->function,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,38 @@ 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,
|
||||||
public string $operator = Query\Where::OPERATOR_EQUAL,
|
public string $operator = Query\Where::OPERATOR_EQUAL,
|
||||||
public string $condition = Query\Where::CONDITION_AND,
|
public string $condition = Query\Where::CONDITION_AND,
|
||||||
public string|\Stringable|array|null $fieldValue = null,
|
public string|\Stringable|array|null $fieldValue = null,
|
||||||
public null|array $generateValue = null,
|
public null|array|\Closure $generateValue = null,
|
||||||
) {
|
) {
|
||||||
$this->field = Attribute::handleArrayField($field);
|
$this->field = Attribute::handleArrayField($field);
|
||||||
$this->fieldValue = Attribute::handleArrayField($fieldValue);
|
$this->fieldValue = Attribute::handleArrayField($fieldValue, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ]);
|
||||||
}
|
}
|
||||||
|
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 {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->fieldValue ?? $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Cache;
|
||||||
|
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
|
trait CacheEventTrait
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected CacheInterface $cache,
|
||||||
|
protected string $cacheKey,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function purgeCache() : void
|
||||||
|
{
|
||||||
|
$keys = $this->cache->get($this->cacheKey);
|
||||||
|
|
||||||
|
if ( $keys && is_iterable($keys) ) {
|
||||||
|
$this->cache->deleteMultiple(array_map(fn($e) => sprintf("%s:%s:", $this->cacheKey, $e), $keys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus;
|
||||||
|
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
use Ulmus\Entity\EntityInterface;
|
||||||
|
use Ulmus\QueryBuilder\QueryBuilderInterface;
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
trait CacheTrait
|
||||||
|
{
|
||||||
|
protected CacheInterface $cache;
|
||||||
|
|
||||||
|
public function attachCachingObject(CacheInterface $cache) : self
|
||||||
|
{
|
||||||
|
$cacheKey = "";
|
||||||
|
$this->cache = $cache;
|
||||||
|
|
||||||
|
# Reading from cache
|
||||||
|
$this->eventRegister(new class($cacheKey) implements Event\Repository\CollectionFromQueryDatasetInterface {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected string & $cacheKey
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, array &$data): void
|
||||||
|
{
|
||||||
|
$this->cacheKey = $repository->queryBuilder->hashSerializedQuery();
|
||||||
|
$data = $repository->getFromCache( $this->cacheKey) ?: [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
# Setting to cache
|
||||||
|
$this->eventRegister(new class($cacheKey) implements Event\Repository\CollectionFromQueryInterface {
|
||||||
|
public function __construct(
|
||||||
|
protected string & $cacheKey
|
||||||
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, EntityCollection $collection): EntityCollection
|
||||||
|
{
|
||||||
|
$repository->setToCache( $this->cacheKey, $collection->map(fn(EntityInterface $e) => $e->entityGetDataset(false, true)));
|
||||||
|
$this->cacheKey = "";
|
||||||
|
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Insert {
|
||||||
|
use Cache\CacheEventTrait;
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, object|array $entity, ?array $dataset = null, bool $replace = false): void
|
||||||
|
{
|
||||||
|
$this->purgeCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
# Cache invalidation
|
||||||
|
$this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Update {
|
||||||
|
use Cache\CacheEventTrait;
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, object|array $entity, array $dataset, array $diff, bool $replace = false): void
|
||||||
|
{
|
||||||
|
$this->purgeCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Delete {
|
||||||
|
use Cache\CacheEventTrait;
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, EntityInterface $entity): void
|
||||||
|
{
|
||||||
|
$this->purgeCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->eventRegister(new class($this->cache, $this->entityCacheKey()) implements Event\Query\Truncate {
|
||||||
|
use Cache\CacheEventTrait;
|
||||||
|
|
||||||
|
public function execute(RepositoryInterface $repository, ?string $table = null, ?string $alias = null, ?string $schema = null): void
|
||||||
|
{
|
||||||
|
$this->purgeCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function entityCacheKey() : string
|
||||||
|
{
|
||||||
|
return sprintf("%s.%s", $this->entityResolver->databaseName(), $this->entityResolver->tableName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFromCache(string $key) : mixed
|
||||||
|
{
|
||||||
|
$keys = $this->cache->get($this->entityCacheKey(), []);
|
||||||
|
|
||||||
|
if (in_array($key, $keys)) {
|
||||||
|
return $this->cache->get(sprintf("%s:%s:", $this->entityCacheKey(), $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setToCache(string $key, mixed $value) : void
|
||||||
|
{
|
||||||
|
$keys = $this->cache->get($this->entityCacheKey(), []);
|
||||||
|
|
||||||
|
if (! in_array($key, $keys)) {
|
||||||
|
$keys[] = $key;
|
||||||
|
|
||||||
|
$this->cache->set($this->entityCacheKey(), $keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cache->set(sprintf("%s:%s:", $this->entityCacheKey(), $key), $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-16
@@ -10,21 +10,13 @@ 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
|
||||||
{
|
{
|
||||||
@@ -32,7 +24,17 @@ class EntityField implements WhereRawParameter
|
|||||||
|
|
||||||
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -72,7 +73,6 @@ class EntityResolver {
|
|||||||
$key = strtolower($item->name);
|
$key = strtolower($item->name);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case static::KEY_ENTITY_NAME:
|
case static::KEY_ENTITY_NAME:
|
||||||
$key = $item->name;
|
$key = $item->name;
|
||||||
break;
|
break;
|
||||||
@@ -95,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;
|
||||||
|
|
||||||
@@ -111,8 +111,22 @@ 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
|
||||||
@@ -215,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
|
||||||
|
|||||||
+22
-25
@@ -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,15 +43,20 @@ 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) )) {
|
||||||
return $this->execute($statement, $parameters, true);
|
return $this->execute($statement, $parameters, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(\PDOException $pdo) {
|
||||||
|
if (!str_starts_with($pdo->getMessage(), 'There is no active transaction')) {
|
||||||
|
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;
|
||||||
@@ -105,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
|
||||||
@@ -120,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,18 +21,21 @@ 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
|
||||||
{
|
{
|
||||||
return parent::beginTransaction();
|
return parent::beginTransaction();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -84,8 +84,12 @@ abstract class Sql {
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parameter($value) : string
|
public static function collate(string $name) : object
|
||||||
{
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::raw(sprintf("COLLATE %s", $name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
-14
@@ -2,26 +2,21 @@
|
|||||||
|
|
||||||
namespace Ulmus;
|
namespace Ulmus;
|
||||||
|
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
use Ulmus\Adapter\AdapterInterface;
|
use Ulmus\Adapter\AdapterInterface;
|
||||||
|
|
||||||
use Ulmus\Common\PdoObject;
|
|
||||||
|
|
||||||
class ConnectionAdapter
|
class ConnectionAdapter
|
||||||
{
|
{
|
||||||
public string $name;
|
|
||||||
|
|
||||||
public array $configuration;
|
|
||||||
|
|
||||||
protected AdapterInterface $adapter;
|
protected AdapterInterface $adapter;
|
||||||
|
|
||||||
protected PdoObject $pdo;
|
protected \PDO $pdo;
|
||||||
|
|
||||||
public function __construct(string $name = "default", array $configuration = [], bool $default = false)
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
|
|
||||||
$this->configuration = $configuration;
|
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public string $name = "default",
|
||||||
|
protected array $configuration = [],
|
||||||
|
public bool $default = false,
|
||||||
|
public ? CacheInterface $cacheObject = null
|
||||||
|
) {
|
||||||
Ulmus::registerAdapter($this, $default);
|
Ulmus::registerAdapter($this, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,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 {
|
||||||
@@ -62,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,7 +112,7 @@ 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 ] );
|
||||||
@@ -116,7 +120,9 @@ class DatasetHandler
|
|||||||
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 = [];
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
namespace Ulmus\Entity;
|
namespace Ulmus\Entity;
|
||||||
|
|
||||||
use Ulmus\Common\{ EntityField, EntityResolver };
|
use Ulmus\Common\{ EntityField, EntityResolver };
|
||||||
use Ulmus\{ ConnectionAdapter, EntityCollection, QueryBuilder\QueryBuilderInterface, Repository };
|
use Ulmus\{ConnectionAdapter,
|
||||||
|
EntityCollection,
|
||||||
|
QueryBuilder\QueryBuilderInterface,
|
||||||
|
Repository,
|
||||||
|
SearchRequest\SearchableInterface};
|
||||||
|
|
||||||
interface EntityInterface /* extends \JsonSerializable */
|
interface EntityInterface extends SearchableInterface /* extends \JsonSerializable */
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Entity\Field;
|
||||||
|
|
||||||
|
class Date extends DatetimeImmutable {
|
||||||
|
|
||||||
|
public string $format = "Y-m-d";
|
||||||
|
}
|
||||||
@@ -4,47 +4,6 @@ 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 {
|
||||||
|
use DatetimeTrait;
|
||||||
public string $format = "Y-m-d H:i:s";
|
|
||||||
|
|
||||||
public function load(...$arguments)
|
|
||||||
{
|
|
||||||
$value = $arguments[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
# From Timestamp
|
|
||||||
if ( is_numeric($value) ) {
|
|
||||||
$obj = new static("@$value");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$obj = new static($value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(\Throwable $ex) {
|
|
||||||
throw new \Exception(sprintf("An error occured trying to instanciate from '%s'. %s", $value, $ex->getMessage()), $ex->getCode(), $ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save()
|
|
||||||
{
|
|
||||||
return $this->__toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString()
|
|
||||||
{
|
|
||||||
return $this->format($this->format);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function formatLocale(string $format) : string
|
|
||||||
{
|
|
||||||
return strftime($format, $this->getTimestamp());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function ageAt(null|\DateTime $dateTime) : int
|
|
||||||
{
|
|
||||||
return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Entity\Field;
|
||||||
|
|
||||||
|
use Ulmus\Entity\EntityObjectInterface;
|
||||||
|
|
||||||
|
class DatetimeImmutable extends \DateTimeImmutable implements EntityObjectInterface {
|
||||||
|
use DatetimeTrait;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Entity\Field;
|
||||||
|
|
||||||
|
trait DatetimeTrait
|
||||||
|
{
|
||||||
|
public string $format = "Y-m-d H:i:s";
|
||||||
|
|
||||||
|
public function load(mixed ...$arguments)
|
||||||
|
{
|
||||||
|
$value = $arguments[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
# From Timestamp
|
||||||
|
if ( is_numeric($value) ) {
|
||||||
|
$obj = new static("@$value");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$obj = new static($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(\Throwable $ex) {
|
||||||
|
throw new \Exception(sprintf("An error occured trying to instanciate from '%s'. %s", $value, $ex->getMessage()), $ex->getCode(), $ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTime($hour, $minute, $second = 0, $microsecond = 0) : \DateTime
|
||||||
|
{
|
||||||
|
$fromParent = parent::setTime($hour, $minute, $second, $microsecond);
|
||||||
|
|
||||||
|
return new static($fromParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
return $this->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->format($this->format);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[\Deprecated]
|
||||||
|
public function formatLocale(string $format) : string
|
||||||
|
{
|
||||||
|
return strftime($format, $this->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatLocaleIntl(int $dateFormatter = \IntlDateFormatter::LONG, int $timeFormatter = \IntlDateFormatter::NONE, ? string $pattern = null) : string
|
||||||
|
{
|
||||||
|
$formatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormatter, $timeFormatter, \date_default_timezone_get(), \IntlDateFormatter::GREGORIAN, $pattern);
|
||||||
|
|
||||||
|
return $formatter->format($this->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ageAt(null|\DateTime $dateTime) : int
|
||||||
|
{
|
||||||
|
return (int) ( (int) ($dateTime ?: new DateTime())->format('Ymd') - (int) $this->format('Ymd') ) / 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): mixed
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Entity\Field;
|
||||||
|
|
||||||
|
class Time extends DatetimeImmutable {
|
||||||
|
|
||||||
|
public string $format = "H:i:s";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ namespace Ulmus\Entity\InformationSchema;
|
|||||||
use Ulmus\EntityCollection,
|
use Ulmus\EntityCollection,
|
||||||
Ulmus\Entity\Field\Datetime;
|
Ulmus\Entity\Field\Datetime;
|
||||||
|
|
||||||
use Ulmus\{Attribute\Obj\Table as TableObj};
|
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;
|
||||||
|
|
||||||
@@ -83,6 +83,12 @@ class Table
|
|||||||
public ? string $temporary;
|
public ? string $temporary;
|
||||||
|
|
||||||
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
|
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
|
||||||
#[Where('TABLE_SCHEMA', fieldValue: [ Column::class, 'tableSchema' ])]
|
#[Where(field: 'TABLE_SCHEMA', generateValue: [ Table::class, 'getSchema' ])]
|
||||||
public EntityCollection $columns;
|
public EntityCollection $columns;
|
||||||
|
|
||||||
|
# Awaiting PHP 8.5 https://wiki.php.net/rfc/closures_in_const_expr
|
||||||
|
public static function getSchema(Table $entity) : string
|
||||||
|
{
|
||||||
|
return $entity->schema;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Entity;
|
||||||
|
|
||||||
|
interface JsonUnserializable extends \JsonSerializable
|
||||||
|
{
|
||||||
|
public function jsonUnserialize(array $json): void;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,6 @@ use Ulmus\Attribute\Property\{Field, Filter, FilterJoin, Relation, Join, Virtual
|
|||||||
class Table extends \Ulmus\Entity\InformationSchema\Table
|
class Table extends \Ulmus\Entity\InformationSchema\Table
|
||||||
{
|
{
|
||||||
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
|
#[Relation(type: "oneToMany", key: "name", foreignKey: [ Column::class, 'tableName' ], entity: Column::class)]
|
||||||
#[Where('TABLE_SCHEMA', fieldValue: [ Column::class, 'tableSchema' ])]
|
#[Where('TABLE_SCHEMA', generateValue: [ Table::class, 'getSchema' ])]
|
||||||
public EntityCollection $columns;
|
public EntityCollection $columns;
|
||||||
}
|
}
|
||||||
@@ -8,26 +8,58 @@ class ObjectInstanciator {
|
|||||||
|
|
||||||
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 ) {
|
else {
|
||||||
|
$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);
|
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 {
|
else {
|
||||||
return new $type(...$arguments);
|
return new $type(...$arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function convert(object $obj)
|
public function convert(object $obj)
|
||||||
{
|
{
|
||||||
if ( $obj instanceof EntityObjectInterface ) {
|
if ( $obj instanceof EntityObjectInterface ) {
|
||||||
return $obj->save();
|
return $obj->save();
|
||||||
}
|
}
|
||||||
|
elseif ( $obj instanceof \JsonSerializable ) {
|
||||||
|
return json_encode($obj->jsonSerialize());
|
||||||
|
}
|
||||||
|
elseif ( $obj instanceof \Stringable ) {
|
||||||
return (string) $obj;
|
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)
|
||||||
{
|
{
|
||||||
if (! $obj instanceof \BackedEnum) {
|
if (! $obj instanceof \BackedEnum) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
+152
-36
@@ -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, $generator, $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 ) {
|
||||||
@@ -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);
|
$key = key($pkField);
|
||||||
|
|
||||||
return isset($this->$key);
|
return isset($this->$key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Exception\EntityPrimaryKeyUnknown(sprintf("Entity %s has no field containing attributes 'primary_key'", static::class));
|
||||||
|
}
|
||||||
|
|
||||||
#[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]
|
||||||
@@ -199,10 +268,57 @@ trait EntityTrait {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Ignore]
|
||||||
|
public function __debugInfo() : array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->toArray(true) ;
|
||||||
|
}
|
||||||
|
catch (\Throwable $ex) {
|
||||||
|
return $this->entityGetDataset(false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ "An error occured trying to export this dataset..." ];
|
||||||
|
}
|
||||||
|
|
||||||
#[Ignore]
|
#[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 +328,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 +349,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 ? '' : 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]
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Entity;
|
||||||
|
|
||||||
|
interface DatasetFill {
|
||||||
|
public function execute(\Generator $generator, \Ulmus\Entity\EntityInterface $entity) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Entity;
|
||||||
|
|
||||||
|
interface DatasetPull {
|
||||||
|
public function execute(\Generator $generator, \Ulmus\Entity\EntityInterface $entity) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Alter {
|
||||||
|
public function execute(RepositoryInterface $repository, array $fields) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Create {
|
||||||
|
public function execute(RepositoryInterface $repository) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Entity\EntityInterface;
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Delete {
|
||||||
|
public function execute(RepositoryInterface $repository, EntityInterface $entity) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Insert {
|
||||||
|
public function execute(RepositoryInterface $repository, object|array $entity, ? array $dataset = null, bool $replace = false) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Truncate {
|
||||||
|
public function execute(RepositoryInterface $repository, ? string $table = null, ? string $alias = null, ? string $schema = null) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Query;
|
||||||
|
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface Update {
|
||||||
|
public function execute(RepositoryInterface $repository, object|array $entity, array $dataset, array $diff, bool $replace = false) : void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Event\Repository;
|
||||||
|
|
||||||
|
use Ulmus\EntityCollection;
|
||||||
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
|
|
||||||
|
interface CollectionFromQueryDatasetInterface {
|
||||||
|
public function execute(RepositoryInterface $repository, array &$data) : void;
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Ulmus\Event\Repository;
|
namespace Ulmus\Event\Repository;
|
||||||
|
|
||||||
use Ulmus\EntityCollection;
|
use Ulmus\{ EntityCollection, Repository\RepositoryInterface };
|
||||||
|
|
||||||
interface CollectionFromQueryInterface {
|
interface CollectionFromQueryInterface {
|
||||||
public function execute(EntityCollection $collection) : EntityCollection;
|
public function execute(RepositoryInterface $repository, EntityCollection $collection) : EntityCollection;
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,10 @@ class FieldDefinition {
|
|||||||
$default = $this->getDefault();
|
$default = $this->getDefault();
|
||||||
$syntax = $this->adapter->tableSyntax();
|
$syntax = $this->adapter->tableSyntax();
|
||||||
|
|
||||||
|
if (is_callable($default)) {
|
||||||
|
$default = "(" . call_user_func($default) . ")";
|
||||||
|
}
|
||||||
|
|
||||||
return implode(' ', array_filter([
|
return implode(' ', array_filter([
|
||||||
$this->isUnsigned() ? $syntax['unsigned'] : null,
|
$this->isUnsigned() ? $syntax['unsigned'] : null,
|
||||||
$this->nullable ? "NULL" : "NOT NULL",
|
$this->nullable ? "NULL" : "NOT NULL",
|
||||||
|
|||||||
@@ -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' ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Pdo;
|
||||||
|
|
||||||
|
class Mssql extends \PDO
|
||||||
|
{
|
||||||
|
use PdoTrait;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Pdo;
|
||||||
|
|
||||||
|
class Mysql extends \PDO\Mysql
|
||||||
|
{
|
||||||
|
use PdoTrait;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Pdo;
|
||||||
|
|
||||||
|
class Sqlite extends \PDO\Sqlite
|
||||||
|
{
|
||||||
|
use PdoTrait, SqlitePdoTrait;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class Having extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# whitelisting operators
|
# whitelisting operators
|
||||||
return in_array(strtoupper($this->operator), [ '=', '!=', '<>', 'LIKE' ]) ? $this->operator : "=";
|
return in_array(strtoupper($this->operator), [ '=', '!=', '>', '>=', '<', '<=', '<>', 'LIKE', 'IS', 'IS NOT', 'REGEXP', 'IN' ]) ? $this->operator : "=";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function value()
|
protected function value()
|
||||||
|
|||||||
+12
-3
@@ -8,6 +8,9 @@ class OrderBy extends Fragment {
|
|||||||
public array $orderBy = [];
|
public array $orderBy = [];
|
||||||
|
|
||||||
const SQL_TOKEN = "ORDER BY";
|
const SQL_TOKEN = "ORDER BY";
|
||||||
|
const SQL_COLLATE = "COLLATE";
|
||||||
|
|
||||||
|
# const 'NULLS LAST|FIRST' must be handlded
|
||||||
|
|
||||||
public function set(array $order) : self
|
public function set(array $order) : self
|
||||||
{
|
{
|
||||||
@@ -16,9 +19,9 @@ class OrderBy extends Fragment {
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add(object|string $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 ];
|
||||||
|
|
||||||
@@ -30,7 +33,7 @@ class OrderBy extends Fragment {
|
|||||||
$list = array_map(function($item) {
|
$list = array_map(function($item) {
|
||||||
list($field, $direction) = $item;
|
list($field, $direction) = $item;
|
||||||
|
|
||||||
return $field . ( $direction ? " $direction" : "" );
|
return implode(' ', array_filter([ $field , $direction ?? null ] ));
|
||||||
}, $this->orderBy);
|
}, $this->orderBy);
|
||||||
|
|
||||||
return $this->renderSegments([
|
return $this->renderSegments([
|
||||||
@@ -38,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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -15,6 +15,7 @@ class Where extends Fragment {
|
|||||||
const CONDITION_OR = "OR";
|
const CONDITION_OR = "OR";
|
||||||
const CONDITION_NOT = "NOT";
|
const CONDITION_NOT = "NOT";
|
||||||
const COMPARISON_IN = "IN";
|
const COMPARISON_IN = "IN";
|
||||||
|
const COMPARISON_NOT_IN = "NOT IN";
|
||||||
const COMPARISON_IS = "IS";
|
const COMPARISON_IS = "IS";
|
||||||
const COMPARISON_NULL = "NULL";
|
const COMPARISON_NULL = "NULL";
|
||||||
|
|
||||||
@@ -100,11 +101,13 @@ class Where extends Fragment {
|
|||||||
{
|
{
|
||||||
$value = $this->value();
|
$value = $this->value();
|
||||||
|
|
||||||
|
$operator = $this->operator();
|
||||||
|
|
||||||
return $this->content ?: $this->content = implode(" ", array_filter([
|
return $this->content ?: $this->content = implode(" ", array_filter([
|
||||||
$this->condition,
|
$this->condition,
|
||||||
$this->not ? Where::CONDITION_NOT : "",
|
$this->not ? Where::CONDITION_NOT : "",
|
||||||
$this->field,
|
$this->field,
|
||||||
$this->operator(),
|
$operator,
|
||||||
$value,
|
$value,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
@@ -112,7 +115,11 @@ class Where extends Fragment {
|
|||||||
protected function operator() : string
|
protected function operator() : string
|
||||||
{
|
{
|
||||||
if ( is_array($this->value) ) {
|
if ( is_array($this->value) ) {
|
||||||
return Where::COMPARISON_IN;
|
if (true === $not = $this->not) {
|
||||||
|
$this->not = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $not ? Where::COMPARISON_NOT_IN : Where::COMPARISON_IN;
|
||||||
}
|
}
|
||||||
|
|
||||||
# whitelisting operators
|
# whitelisting operators
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ namespace Ulmus;
|
|||||||
|
|
||||||
class QueryBuilder extends QueryBuilder\Sql\MysqlQueryBuilder
|
class QueryBuilder extends QueryBuilder\Sql\MysqlQueryBuilder
|
||||||
{
|
{
|
||||||
# Backward compatibility defaulting on MySQL/MariaDB query builder
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\QueryBuilder;
|
||||||
|
|
||||||
|
use Ulmus\Common\Sql;
|
||||||
|
use Ulmus\Query;
|
||||||
|
|
||||||
|
trait JsonQueryBuilderTrait
|
||||||
|
{
|
||||||
|
public function whereJsonArray(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self
|
||||||
|
{
|
||||||
|
$uid = uniqid("json_");
|
||||||
|
|
||||||
|
$extract = Sql::function("json_extract", $field, '$');
|
||||||
|
|
||||||
|
$each = Sql::function('json_each', $extract)->as($uid);
|
||||||
|
|
||||||
|
return $this->from( $each )->where(Sql::raw("{$uid}.value"), $value, $operator, $condition, $not);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whereJsonKey(string|\Stringable $field, string|null $key, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self
|
||||||
|
{
|
||||||
|
$uid = uniqid("json_");
|
||||||
|
|
||||||
|
$extract = Sql::function("json_extract", $field, $key ? "$.$key" : "$");
|
||||||
|
|
||||||
|
$each = Sql::function('json_each', $extract)->as($uid);
|
||||||
|
|
||||||
|
return $this->from( $each )->where(Sql::raw("{$uid}.value"), $value, $operator, $condition, $not);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@ interface QueryBuilderInterface
|
|||||||
public function reset() : void;
|
public function reset() : void;
|
||||||
public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface;
|
public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface;
|
||||||
public function removeFragment(QueryFragmentInterface|array|\Stringable|string $fragment) : void;
|
public function removeFragment(QueryFragmentInterface|array|\Stringable|string $fragment) : void;
|
||||||
|
public function hashSerializedQuery() : string;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function orderBy(string|\Stringable $field, ? string $direction = null) : self
|
public function orderBy(string|\Stringable $field, null|string|\Stringable $direction = null) : self
|
||||||
{
|
{
|
||||||
if ( null === $orderBy = $this->getFragment(Query\OrderBy::class) ) {
|
if ( null === $orderBy = $this->getFragment(Query\OrderBy::class) ) {
|
||||||
$orderBy = new Query\OrderBy();
|
$orderBy = new Query\OrderBy();
|
||||||
@@ -272,7 +272,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function groupBy(string|object $field) : self
|
public function groupBy(string|\Stringable $field) : self
|
||||||
{
|
{
|
||||||
if ( null === $groupBy = $this->getFragment(Query\GroupBy::class) ) {
|
if ( null === $groupBy = $this->getFragment(Query\GroupBy::class) ) {
|
||||||
$groupBy = new Query\GroupBy();
|
$groupBy = new Query\GroupBy();
|
||||||
@@ -432,8 +432,12 @@ class MysqlQueryBuilder extends SqlQueryBuilder
|
|||||||
return array_shift($this->queryStack);
|
return array_shift($this->queryStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(bool $skipToken = false) /* : mixed */
|
public function render(bool $skipToken = false) : mixed
|
||||||
{
|
{
|
||||||
|
if (isset($this->rendered)) {
|
||||||
|
return $this->rendered;
|
||||||
|
}
|
||||||
|
|
||||||
$sql = [];
|
$sql = [];
|
||||||
|
|
||||||
usort($this->queryStack, function($q1, $q2) {
|
usort($this->queryStack, function($q1, $q2) {
|
||||||
@@ -444,7 +448,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
|
|||||||
$sql[] = $fragment->render($skipToken);
|
$sql[] = $fragment->render($skipToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return implode(" ", $sql);
|
return $this->rendered = implode(" ", $sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reset() : void
|
public function reset() : void
|
||||||
@@ -453,8 +457,7 @@ class MysqlQueryBuilder extends SqlQueryBuilder
|
|||||||
$this->whereConditionOperator = Query\Where::CONDITION_AND;
|
$this->whereConditionOperator = Query\Where::CONDITION_AND;
|
||||||
$this->havingConditionOperator = Query\Where::CONDITION_AND;
|
$this->havingConditionOperator = Query\Where::CONDITION_AND;
|
||||||
$this->parameterIndex = 0;
|
$this->parameterIndex = 0;
|
||||||
|
unset($this->where, $this->having, $this->rendered, $this->hash);
|
||||||
unset($this->where, $this->having);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface
|
public function getFragment(string $class, int $index = 0) : ? QueryFragmentInterface
|
||||||
@@ -488,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;
|
||||||
}
|
}
|
||||||
@@ -498,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);
|
||||||
@@ -537,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,11 @@ 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 $hash;
|
||||||
|
|
||||||
public function push(QueryFragmentInterface $queryFragment): QueryBuilderInterface
|
public function push(QueryFragmentInterface $queryFragment): QueryBuilderInterface
|
||||||
{
|
{
|
||||||
@@ -18,7 +21,7 @@ class SqlQueryBuilder implements QueryBuilderInterface
|
|||||||
// TODO: Implement pull() method.
|
// TODO: Implement pull() method.
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(bool $skipToken = false)
|
public function render(bool $skipToken = false) : mixed
|
||||||
{
|
{
|
||||||
// TODO: Implement render() method.
|
// TODO: Implement render() method.
|
||||||
}
|
}
|
||||||
@@ -37,4 +40,9 @@ class SqlQueryBuilder implements QueryBuilderInterface
|
|||||||
{
|
{
|
||||||
// TODO: Implement removeFragment() method.
|
// TODO: Implement removeFragment() method.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hashSerializedQuery(): string
|
||||||
|
{
|
||||||
|
return $this->hash ??= md5(sprintf("%s:%s", $this->render(), serialize($this->parameters)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+137
-503
@@ -5,38 +5,53 @@ namespace Ulmus;
|
|||||||
use Ulmus\Attribute\Property\{
|
use Ulmus\Attribute\Property\{
|
||||||
Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin
|
Field, OrderBy, Where, Having, Relation, Filter, Join, FilterJoin, WithJoin
|
||||||
};
|
};
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
use Ulmus\Common\EntityResolver;
|
use Ulmus\Common\EntityResolver;
|
||||||
|
use Ulmus\Entity\EntityInterface;
|
||||||
|
use Ulmus\Repository\JoinBuilder;
|
||||||
use Ulmus\Repository\RepositoryInterface;
|
use Ulmus\Repository\RepositoryInterface;
|
||||||
use Ulmus\Repository\WithOptionEnum;
|
use Ulmus\Repository\WithOptionEnum;
|
||||||
|
|
||||||
class Repository implements RepositoryInterface
|
class Repository implements RepositoryInterface
|
||||||
{
|
{
|
||||||
use EventTrait, Repository\ConditionTrait, Repository\EscapeTrait;
|
use EventTrait, CacheTrait, Repository\ConditionTrait, Repository\EscapeTrait, Repository\QueryBuildingTrait, Repository\JoinBuilderTrait;
|
||||||
|
|
||||||
const DEFAULT_ALIAS = "this";
|
const DEFAULT_ALIAS = "this";
|
||||||
|
|
||||||
public ? ConnectionAdapter $adapter;
|
|
||||||
|
|
||||||
public string $alias;
|
public string $alias;
|
||||||
|
|
||||||
public string $entityClass;
|
public string $entityClass;
|
||||||
|
|
||||||
public array $events = [];
|
public array $events = [];
|
||||||
|
|
||||||
protected QueryBuilder\QueryBuilderInterface $queryBuilder;
|
public ? ConnectionAdapter $adapter;
|
||||||
|
|
||||||
|
public readonly QueryBuilder\QueryBuilderInterface $queryBuilder;
|
||||||
|
|
||||||
protected EntityResolver $entityResolver;
|
protected EntityResolver $entityResolver;
|
||||||
|
|
||||||
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);
|
||||||
$this->adapter = $adapter ?? $this->entityResolver->databaseAdapter();
|
|
||||||
|
|
||||||
$queryBuilder = $this->adapter->adapter()->queryBuilderClass();
|
if ($adapter) {
|
||||||
$this->queryBuilder = new $queryBuilder();
|
$this->adapter = $adapter;
|
||||||
|
|
||||||
|
$qbClass = $adapter->adapter()->queryBuilderClass();
|
||||||
|
$this->queryBuilder = new $qbClass();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->adapter = $this->entityResolver->databaseAdapter();
|
||||||
|
$this->queryBuilder = $this->entityClass::queryBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->adapter->cacheObject) {
|
||||||
|
$this->attachCachingObject($this->adapter->cacheObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __clone()
|
public function __clone()
|
||||||
@@ -46,7 +61,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
|
||||||
@@ -64,6 +79,7 @@ class Repository implements RepositoryInterface
|
|||||||
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);
|
||||||
@@ -71,7 +87,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
|
||||||
@@ -84,7 +100,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))));
|
||||||
@@ -113,6 +129,8 @@ class Repository implements RepositoryInterface
|
|||||||
|
|
||||||
public function deleteAll()
|
public function deleteAll()
|
||||||
{
|
{
|
||||||
|
$this->eventExecute(Event\Query\Delete::class, $this);
|
||||||
|
|
||||||
return $this->deleteSqlQuery()->runDeleteQuery();
|
return $this->deleteSqlQuery()->runDeleteQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +143,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) ) {
|
||||||
@@ -133,14 +160,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);
|
||||||
|
|
||||||
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
|
||||||
@@ -164,7 +202,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) ) {
|
||||||
@@ -178,6 +216,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 ) {
|
||||||
@@ -193,24 +235,37 @@ class Repository implements RepositoryInterface
|
|||||||
|
|
||||||
$entity->entityFillFromDataset($dataset, true);
|
$entity->entityFillFromDataset($dataset, true);
|
||||||
|
|
||||||
|
$this->eventExecute(Event\Query\Insert::class, $this, $entity, $dataset, $replace);
|
||||||
|
|
||||||
return (bool) ( $pkValue ?? $pdoObject->lastInsertId );
|
return (bool) ( $pkValue ?? $pdoObject->lastInsertId );
|
||||||
}
|
}
|
||||||
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 ) {
|
||||||
|
if ($primaryKeyDefinition) {
|
||||||
$pkField = key($primaryKeyDefinition);
|
$pkField = key($primaryKeyDefinition);
|
||||||
$pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField;
|
$pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField;
|
||||||
$this->where($pkFieldName, $dataset[$pkFieldName]);
|
$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);
|
||||||
|
|
||||||
|
$this->eventExecute(Event\Query\Update::class, $this, $entity, $dataset, $diff, $replace);
|
||||||
|
|
||||||
return $update ? (bool) $update->rowCount : false;
|
return $update ? (bool) $update->rowCount : false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,64 +284,6 @@ class Repository implements RepositoryInterface
|
|||||||
return $changed;
|
return $changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function insertAll(EntityCollection|array $collection, int $size = 1000) : int
|
|
||||||
{
|
|
||||||
if ( empty($collection) ) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
elseif ( is_array($collection) ) {
|
|
||||||
$collection = $this->entityClass::entityCollection($collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($collection as $entity) {
|
|
||||||
if ( ! $this->matchEntity($entity) ) {
|
|
||||||
throw new \Exception("Your entity class `" . get_class($entity) . "` cannot match entity type of repository `{$this->entityClass}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$dataset = $entity->toArray();
|
|
||||||
|
|
||||||
$primaryKeyDefinition = Ulmus::resolveEntity($this->entityClass)->getPrimaryKeyField();
|
|
||||||
|
|
||||||
if ( ! $entity->isLoaded() ) {
|
|
||||||
# $dataset = array_filter($dataset, fn($item, $field) => ! ($this->entityResolver->searchFieldAnnotation($field, new Field, false)->readonly ?? false), \ARRAY_FILTER_USE_BOTH);
|
|
||||||
|
|
||||||
$statement = $this->insertSqlQuery($fieldsAndValue ?? $dataset, $replace)->runInsertQuery();
|
|
||||||
|
|
||||||
if ( ( 0 !== $statement->lastInsertId ) &&
|
|
||||||
( null !== $primaryKeyDefinition )) {
|
|
||||||
|
|
||||||
$pkField = key($primaryKeyDefinition);
|
|
||||||
$dataset[$pkField] = $statement->lastInsertId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$entity->entityFillFromDataset($dataset, true);
|
|
||||||
|
|
||||||
return (bool) $statement->lastInsertId;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ( $primaryKeyDefinition === null ) {
|
|
||||||
throw new \Exception(sprintf("No primary key found for entity %s", $this->entityClass));
|
|
||||||
}
|
|
||||||
|
|
||||||
$diff = $fieldsAndValue ?? $this->generateWritableDataset($entity);
|
|
||||||
|
|
||||||
if ( [] !== $diff ) {
|
|
||||||
$pkField = key($primaryKeyDefinition);
|
|
||||||
$pkFieldName = $primaryKeyDefinition[$pkField]->name ?? $pkField;
|
|
||||||
$this->where($pkFieldName, $dataset[$pkFieldName]);
|
|
||||||
|
|
||||||
$update = $this->updateSqlQuery($diff)->runUpdateQuery();
|
|
||||||
|
|
||||||
$entity->entityFillFromDataset($dataset, true);
|
|
||||||
|
|
||||||
return $update ? (bool) $update->rowCount : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function replace(object|array $entity, ? array $fieldsAndValue = null) : bool
|
public function replace(object|array $entity, ? array $fieldsAndValue = null) : bool
|
||||||
{
|
{
|
||||||
return $this->save($entity, $fieldsAndValue, true);
|
return $this->save($entity, $fieldsAndValue, true);
|
||||||
@@ -303,35 +300,34 @@ class Repository implements RepositoryInterface
|
|||||||
return $changed;
|
return $changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self
|
|
||||||
|
public function createTable() : mixed
|
||||||
{
|
{
|
||||||
$schema = $schema ?: $this->entityResolver->schemaName();
|
$this->eventExecute(Event\Query\Create::class, $this);
|
||||||
|
|
||||||
$this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $this->escapeIdentifier($alias ?: $this->alias), $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null);
|
|
||||||
|
|
||||||
$this->finalizeQuery();
|
|
||||||
|
|
||||||
# ??? $result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createTable()
|
|
||||||
{
|
|
||||||
return $this->createSqlQuery()->runQuery();
|
return $this->createSqlQuery()->runQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function alterTable(array $fields)
|
public function alterTable(array $fields) : mixed
|
||||||
{
|
{
|
||||||
|
$this->eventExecute(Event\Query\Alter::class, $this, $fields);
|
||||||
|
|
||||||
return $this->alterSqlQuery($fields)->runQuery();
|
return $this->alterSqlQuery($fields)->runQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listTables(? string $database = null)
|
public function truncateTable() : mixed
|
||||||
|
{
|
||||||
|
$this->eventExecute(Event\Query\Truncate::class, $this, $table, $alias, $schema);
|
||||||
|
|
||||||
|
return $this->truncate()->runQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listTables(? string $database = null) : mixed
|
||||||
{
|
{
|
||||||
return $this->showTablesSqlQuery($database)->runQuery();
|
return $this->showTablesSqlQuery($database)->runQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listColumns(? string $table = null)
|
public function listColumns(? string $table = null) : EntityCollection
|
||||||
{
|
{
|
||||||
$table ??= $this->entityResolver->tableName();
|
$table ??= $this->entityResolver->tableName();
|
||||||
|
|
||||||
@@ -342,7 +338,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));
|
||||||
|
|
||||||
@@ -356,12 +352,21 @@ class Repository implements RepositoryInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string) $e1 !== (string) $e2;
|
if (is_null($e1) || is_null($e2)) {
|
||||||
});
|
return $e1 !== $e2;
|
||||||
|
|
||||||
# return array_diff_assoc($oldValues ? $dataset : $array , $oldValues ? $array : $dataset );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($e1 instanceof \BackedEnum) {
|
||||||
|
$e1 = $e1->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($e2 instanceof \BackedEnum) {
|
||||||
|
$e2 = $e2->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $e1 !== (string) $e2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function generateWritableDataset(object $entity, bool $oldValues = false) : array
|
public function generateWritableDataset(object $entity, bool $oldValues = false) : array
|
||||||
{
|
{
|
||||||
@@ -370,7 +375,7 @@ class Repository implements RepositoryInterface
|
|||||||
$dataset = $this->generateDatasetDiff($entity, $oldValues);
|
$dataset = $this->generateDatasetDiff($entity, $oldValues);
|
||||||
|
|
||||||
foreach($dataset as $field => $value) {
|
foreach($dataset as $field => $value) {
|
||||||
if ( false === ( $this->entityResolver->searchFieldAnnotation($field, [ Field::class, Field::class ], false)->readonly ?? false ) ) {
|
if ( false === ( $this->entityResolver->searchFieldAnnotation($field, Field::class, false)->readonly ?? false ) ) {
|
||||||
$intersect[$field] = $field;
|
$intersect[$field] = $field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,21 +394,13 @@ class Repository implements RepositoryInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeQueryFragment(null|Query\QueryFragmentInterface|string|array $fragment) : self
|
|
||||||
{
|
|
||||||
foreach((array) $fragment as $item) {
|
|
||||||
$this->queryBuilder->removeFragment($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function selectEntity(string $entity, string $alias, string $prependField = "") : self
|
public function selectEntity(string $entity, string $alias, string $prependField = "") : self
|
||||||
{
|
{
|
||||||
$prependField and ($prependField .= "$");
|
$prependField and ($prependField .= "$");
|
||||||
|
|
||||||
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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,167 +428,6 @@ class Repository implements RepositoryInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function select(array|string|\Stringable $fields, bool $distinct = false) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->select($fields, $distinct);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function distinct(array|string|\Stringable $fields) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->select($fields);
|
|
||||||
$this->queryBuilder->getFragment(Query\Select::class)->distinct = true;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insert(array $fieldlist, string $table, string $alias, ? string $schema, bool $replace = false) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->insert($fieldlist, $this->escapeTable($table), $this->escapeIdentifier($alias), $this->escapedDatabase(), $schema, $replace);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function values(array $dataset) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->values($dataset);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(string $table, string $alias, ? string $schema) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->update($this->escapeTable($table), $alias ? $this->escapeIdentifier($alias) : null, $this->escapedDatabase(), $schema);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function set(array $dataset) : self
|
|
||||||
{
|
|
||||||
$keys = array_keys($dataset);
|
|
||||||
$escapedFields = array_combine($keys, array_map([ $this, 'escapeField' ], $keys));
|
|
||||||
|
|
||||||
$this->queryBuilder->set($dataset, $escapedFields);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(...$args) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->delete();
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function from(string $table, ? string $alias, ? string $schema) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->from($this->escapeTable($table), $alias ? $this->escapeIdentifier($alias) : null, $this->escapedDatabase(), $schema ? $this->escapeSchema($schema) : null);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function join(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self
|
|
||||||
{
|
|
||||||
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias ? $this->escapeIdentifier($alias) : null);
|
|
||||||
|
|
||||||
if ( $callback ) {
|
|
||||||
$callback($join);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function outerJoin(string $type, $table, string|\Stringable $field, mixed $value, ? string $alias = null, ? callable $callback = null) : self
|
|
||||||
{
|
|
||||||
$join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias ? $this->escapeIdentifier($alias) : null);
|
|
||||||
|
|
||||||
if ( $callback ) {
|
|
||||||
$callback($join);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function match() : self
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notMatch() : self
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function between() : self
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notBetween() : self
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupBy(string|\Stringable $field) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->groupBy($field);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groups(array $groups) : self
|
|
||||||
{
|
|
||||||
foreach($groups as $field ) {
|
|
||||||
$this->groupBy($field);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function orderBy(string|object $field, ? string $direction = null) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->orderBy($field, $direction);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
# @UNTESTED
|
|
||||||
public function randomizeOrder() : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->orderBy(Common\Sql::function('RAND', Sql::identifier('CURDATE()+0')));
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function orders(array $orderList) : self
|
|
||||||
{
|
|
||||||
foreach($orderList as $field => $direction) {
|
|
||||||
if (is_numeric($field)) {
|
|
||||||
$this->orderBy($direction);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Associative array with direction
|
|
||||||
$this->orderBy($field, $direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function limit(int $value) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->limit($value);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function offset(int $value) : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->offset($value);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @TODO */
|
/* @TODO */
|
||||||
public function commit() : self
|
public function commit() : self
|
||||||
{
|
{
|
||||||
@@ -615,110 +451,6 @@ class Repository implements RepositoryInterface
|
|||||||
return $this->where($this->entityClass::field($primaryKeyField[$pkField]->name ?? $pkField, $alias), $value);
|
return $this->where($this->entityClass::field($primaryKeyField[$pkField]->name ?? $pkField, $alias), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withJoin(string|array $fields, array $options = []) : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
|
|
||||||
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
|
|
||||||
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select)));
|
|
||||||
}
|
|
||||||
|
|
||||||
# @TODO Apply FILTER annotation to this too !
|
|
||||||
foreach(array_filter((array) $fields) as $item) {
|
|
||||||
if ( isset($this->joined[$item]) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$this->joined[$item] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$attribute = $this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
|
|
||||||
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]);
|
|
||||||
|
|
||||||
$isRelation = ( $attribute instanceof Relation ) || ($attribute instanceof Relation);
|
|
||||||
|
|
||||||
if ($isRelation && ( $attribute->isManyToMany() )) {
|
|
||||||
throw new \Exception("Many-to-many relation can not be preloaded within joins.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $attribute ) {
|
|
||||||
$alias = $attribute->alias ?? $item;
|
|
||||||
|
|
||||||
$entity = $attribute->entity ?? $this->entityResolver->reflectedClass->getProperties(true)[$item]->getTypes()[0]->type;
|
|
||||||
|
|
||||||
foreach($entity::resolveEntity()->fieldList(Common\EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
|
|
||||||
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
|
|
||||||
$escAlias = $this->escapeIdentifier($alias);
|
|
||||||
$fieldName = $this->escapeIdentifier($key);
|
|
||||||
|
|
||||||
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
|
|
||||||
|
|
||||||
|
|
||||||
$this->select("$escAlias.$fieldName as $alias\${$name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->open();
|
|
||||||
|
|
||||||
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
|
|
||||||
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
|
|
||||||
if ( is_object($condition->field) && ( $condition->field->entityClass !== $entity ) ) {
|
|
||||||
$this->where(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
|
|
||||||
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
|
|
||||||
$this->having(is_object($condition->field) ? $condition->field : $entity::field($condition->field), $condition->getValue(), $condition->operator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! in_array(WithOptionEnum::SkipFilter, $options)) {
|
|
||||||
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) {
|
|
||||||
call_user_func_array([$this->entityClass, $filter->method], [$this, $item, true]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->close();
|
|
||||||
|
|
||||||
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
|
|
||||||
|
|
||||||
$foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey;
|
|
||||||
|
|
||||||
$this->join("LEFT", $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $options) {
|
|
||||||
if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) {
|
|
||||||
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
|
|
||||||
if ( ! is_object($condition->field) ) {
|
|
||||||
$field = $this->entityClass::field($condition->field);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$field = clone $condition->field;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Adding directly
|
|
||||||
if ( $field->entityClass === $entity ) {
|
|
||||||
$field->alias = $alias;
|
|
||||||
|
|
||||||
$join->where(is_object($field) ? $field : $entity::field($field, $alias), $condition->getValue(), $condition->operator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) {
|
|
||||||
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) {
|
|
||||||
call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function withSubquery(string|array $fields, array $options = []) : self
|
public function withSubquery(string|array $fields, array $options = []) : self
|
||||||
{
|
{
|
||||||
# We skip subqueries when counting results since it should not affect the row count.
|
# We skip subqueries when counting results since it should not affect the row count.
|
||||||
@@ -727,7 +459,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 !
|
||||||
@@ -804,7 +540,7 @@ class Repository implements RepositoryInterface
|
|||||||
|
|
||||||
foreach ($where as $condition) {
|
foreach ($where as $condition) {
|
||||||
# $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition);
|
# $repository->where($condition->field, is_callable($condition->value) ? call_user_func_array($condition->value, [$this]) : $condition->getValue(), $condition->operator, $condition->condition);
|
||||||
$repository->where($condition->field, $condition->getValue($this), $condition->operator, $condition->condition);
|
$repository->where($condition->field, $condition->getValue(/* why repository sent here ??? $this */), $condition->operator, $condition->condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($order as $item) {
|
foreach ($order as $item) {
|
||||||
@@ -845,20 +581,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
|
||||||
@@ -874,16 +608,20 @@ class Repository implements RepositoryInterface
|
|||||||
|
|
||||||
$this->finalizeQuery();
|
$this->finalizeQuery();
|
||||||
|
|
||||||
foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
|
$dataset = [];
|
||||||
|
|
||||||
|
$this->eventExecute(\Ulmus\Event\Repository\CollectionFromQueryDatasetInterface::class, $this, $dataset);
|
||||||
|
|
||||||
|
foreach($dataset ?: Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
|
||||||
$this->eventExecute(\Ulmus\Event\Repository\CollectionFromQueryItemInterface::class, $entityData);
|
$this->eventExecute(\Ulmus\Event\Repository\CollectionFromQueryItemInterface::class, $entityData);
|
||||||
|
|
||||||
$entity = $this->instanciateEntity($entityClass);
|
$entity = $this->instanciateEntity($entityClass);
|
||||||
$entity->loadedFromAdapter = $this->adapter->name;
|
$entity->loadedFromAdapter = $this->adapter->name;
|
||||||
|
|
||||||
$entityCollection->append( $entity->resetVirtualProperties()->entityFillFromDataset($entityData) );
|
$entityCollection->append( $entity->entityFillFromDataset($entityData) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $entityCollection);
|
$this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $this, $entityCollection);
|
||||||
|
|
||||||
return $entityCollection;
|
return $entityCollection;
|
||||||
}
|
}
|
||||||
@@ -925,110 +663,6 @@ class Repository implements RepositoryInterface
|
|||||||
return Ulmus::runDeleteQuery($this->queryBuilder, $this->adapter);
|
return Ulmus::runDeleteQuery($this->queryBuilder, $this->adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetQuery() : self
|
|
||||||
{
|
|
||||||
$this->queryBuilder->reset();
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function insertSqlQuery(array $dataset, bool $replace = false) : self
|
|
||||||
{
|
|
||||||
if ( null === $insert = $this->queryBuilder->getFragment(Query\Insert::class) ) {
|
|
||||||
$this->insert(array_map([ $this, 'escapeField' ] , array_keys($dataset)), $this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName(), $replace);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$insert->replace = $replace;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->values($dataset);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function updateSqlQuery(array $dataset) : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Update::class) ) {
|
|
||||||
$this->update($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->set($dataset);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function selectSqlQuery() : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
|
|
||||||
$fields = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
|
|
||||||
$this->select($this->entityClass::fields(array_map(fn($f) => $f->object->name ?? $f->name, $fields)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {
|
|
||||||
$this->from($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function deleteSqlQuery() : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Delete::class) ) {
|
|
||||||
$this->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {
|
|
||||||
$this->from($this->entityResolver->tableName(), null, $this->entityResolver->schemaName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createSqlQuery() : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Create::class) ) {
|
|
||||||
$this->queryBuilder->create($this->adapter->adapter(), $this->escapeFieldList($this->entityResolver->fieldList(EntityResolver::KEY_ENTITY_NAME, true)), $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function alterSqlQuery(array $fields) : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Alter::class) ) {
|
|
||||||
$this->queryBuilder->alter($this->adapter->adapter(), $fields, $this->escapeTable($this->entityResolver->tableName()), $this->entityResolver->schemaName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function showDatabasesSqlQuery() : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) {
|
|
||||||
$this->queryBuilder->showDatabases();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function showTablesSqlQuery() : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) {
|
|
||||||
$this->queryBuilder->showTables();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function showColumnsSqlQuery(string $table) : self
|
|
||||||
{
|
|
||||||
if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) {
|
|
||||||
$this->queryBuilder->showColumns($table);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function fromRow($row) : self
|
protected function fromRow($row) : self
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -1039,25 +673,25 @@ class Repository implements RepositoryInterface
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSqlQuery(bool $flush = true) : string
|
|
||||||
{
|
|
||||||
$result = $this->queryBuilder->render();
|
|
||||||
|
|
||||||
$flush and $this->queryBuilder->reset();
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function instanciateEntityCollection(...$arguments) : EntityCollection
|
public function instanciateEntityCollection(...$arguments) : EntityCollection
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
$entityClass ??= $this->entityClass;
|
$entityClass ??= $this->entityClass;
|
||||||
|
|
||||||
return new $entityClass;
|
try {
|
||||||
|
$entity = new $entityClass($dataset);
|
||||||
|
}
|
||||||
|
catch (\Throwable $ex) {
|
||||||
|
$entity = ( new \ReflectionClass($entityClass) )->newInstanceWithoutConstructor()->fromArray($dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity->initializeEntity();
|
||||||
|
|
||||||
|
return $entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasFilters() : bool
|
public function hasFilters() : bool
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -151,7 +151,7 @@ trait ConditionTrait
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeQueryFragment(Query\Fragment|\Stringable|string|array $fragment) : self
|
public function removeQueryFragment(null|Query\QueryFragmentInterface|string|\Stringable|array $fragment) : self
|
||||||
{
|
{
|
||||||
foreach((array) $fragment as $item) {
|
foreach((array) $fragment as $item) {
|
||||||
$this->queryBuilder->removeFragment($item);
|
$this->queryBuilder->removeFragment($item);
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Repository;
|
||||||
|
|
||||||
|
use Ulmus\Attribute\Property\Field;
|
||||||
|
use Ulmus\Attribute\Property\Filter;
|
||||||
|
use Ulmus\Attribute\Property\FilterJoin;
|
||||||
|
use Ulmus\Attribute\Property\Having;
|
||||||
|
use Ulmus\Attribute\Property\Join;
|
||||||
|
use Ulmus\Attribute\Property\Relation;
|
||||||
|
use Ulmus\Attribute\Property\Where;
|
||||||
|
use Ulmus\Common\EntityField;
|
||||||
|
use Ulmus\Common\EntityResolver;
|
||||||
|
use Ulmus\QueryBuilder\QueryBuilderInterface;
|
||||||
|
use Ulmus\Query;
|
||||||
|
|
||||||
|
trait JoinBuilderTrait
|
||||||
|
{
|
||||||
|
public function withJoin(string|array $fields, array $options = []) : self
|
||||||
|
{
|
||||||
|
$defaultAlias = $this->escapeIdentifier(static::DEFAULT_ALIAS);
|
||||||
|
|
||||||
|
$canSelect = null === $this->queryBuilder->getFragment(Query\Select::class);
|
||||||
|
|
||||||
|
if ( $canSelect ) {
|
||||||
|
$select = $this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME, true);
|
||||||
|
|
||||||
|
$this->select($this->entityClass::fields(array_map(fn($f) => $f['object']->name ?? $f['name'], $select), select: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = (array) $fields;
|
||||||
|
|
||||||
|
usort($fields, fn($e1, $e2) => str_contains($e1, '.') <=> str_contains($e2, '.'));
|
||||||
|
|
||||||
|
# @TODO Apply fff annotation to this too !
|
||||||
|
foreach(array_filter($fields, fn($e) => $e && ! isset($this->joined[$e]) ) as $item) {
|
||||||
|
$this->joined[$item] = true;
|
||||||
|
|
||||||
|
$attribute = clone ($this->entityResolver->searchFieldAnnotation($item, [ Join::class ]) ?:
|
||||||
|
$this->entityResolver->searchFieldAnnotation($item, [ Relation::class ]));
|
||||||
|
|
||||||
|
$isRelation = $attribute instanceof Relation;
|
||||||
|
|
||||||
|
if ($isRelation && ( $attribute->isManyToMany() )) {
|
||||||
|
throw new \Exception("Many-to-many relation can not be preloaded within joins.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $attribute ) {
|
||||||
|
$alias = $attribute->alias ?? $item;
|
||||||
|
$entity = $attribute->entity ?? $this->entityResolver->getPropertyEntityType($item);
|
||||||
|
|
||||||
|
foreach($entity::resolveEntity()->fieldList(EntityResolver::KEY_COLUMN_NAME, true) as $key => $field) {
|
||||||
|
if ( null === $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Relation\Ignore::class ]) ) {
|
||||||
|
$escAlias = $this->escapeIdentifier($alias);
|
||||||
|
$fieldName = $this->escapeIdentifier($key);
|
||||||
|
|
||||||
|
$name = $entity::resolveEntity()->searchFieldAnnotation($field->name, [ Field::class ])->name ?? $field->name;
|
||||||
|
|
||||||
|
if ($canSelect) {
|
||||||
|
# @TODO add handling of the new #[Select] attribute here !
|
||||||
|
|
||||||
|
$this->select("$escAlias.$fieldName as $alias\${$name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->open();
|
||||||
|
|
||||||
|
if ( ! in_array(WithOptionEnum::SkipWhere, $options)) {
|
||||||
|
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ] ) as $condition) {
|
||||||
|
$field = $this->joinFieldObject($condition->field, $this->entityClass);
|
||||||
|
|
||||||
|
if ( $field->entityClass === $this->entityClass ) {
|
||||||
|
$this->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array(WithOptionEnum::SkipHaving, $options)) {
|
||||||
|
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Having::class ]) as $condition) {
|
||||||
|
$field = $this->joinFieldObject($condition->field, $this->entityClass);
|
||||||
|
|
||||||
|
$this->having($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array(WithOptionEnum::SkipFilter, $options)) {
|
||||||
|
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ Filter::class ]) as $filter) {
|
||||||
|
call_user_func_array([$this->entityClass, $filter->method], [ $this, $item, true ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->close();
|
||||||
|
|
||||||
|
$key = is_string($attribute->key) ? $this->entityClass::field($attribute->key) : $attribute->key;
|
||||||
|
|
||||||
|
$foreignKey = is_string($attribute->foreignKey) ? $entity::field($attribute->foreignKey, $alias) : $attribute->foreignKey;
|
||||||
|
|
||||||
|
$this->join(Query\Join::TYPE_LEFT, $entity::resolveEntity()->tableName(), $key, $foreignKey, $alias, function($join) use ($item, $entity, $alias, $defaultAlias, $options) {
|
||||||
|
if ( ! in_array(WithOptionEnum::SkipJoinWhere, $options)) {
|
||||||
|
foreach($this->entityResolver->searchFieldAnnotationList($item, [ Where::class ]) as $condition) {
|
||||||
|
$field = $this->joinFieldObject($condition->field, $this->entityClass);
|
||||||
|
|
||||||
|
# Adding directly
|
||||||
|
if ( $field->entityClass === $entity ) {
|
||||||
|
if ($field->alias === $defaultAlias) {
|
||||||
|
$field->alias = $this->escapeIdentifier($alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
$join->where($field, $this->joinFieldValue($condition->getValue(), $alias), $condition->operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array(WithOptionEnum::SkipJoinFilter, $options) ) {
|
||||||
|
foreach ($this->entityResolver->searchFieldAnnotationList($item, [ FilterJoin::class ]) as $filter) {
|
||||||
|
call_user_func_array([$this->entityClass, $filter->method], [$join, $item, true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new \Exception("Referenced field `$item` which do not exist or do not contain a valid @Join or @Relation attribute.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function joinFieldObject(mixed $field, string $entityClass, string $alias = null) : \Stringable
|
||||||
|
{
|
||||||
|
if ( ! is_object($field) ) {
|
||||||
|
return $entityClass::field($field, $alias);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return clone $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected function joinFieldValue(mixed $value, string $alias = null) : mixed
|
||||||
|
{
|
||||||
|
if ( $value instanceof EntityField ) {
|
||||||
|
$value = clone $value;
|
||||||
|
|
||||||
|
$value->alias = $value->alias ?: $this->escapeIdentifier($alias);
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Ulmus\Repository;
|
||||||
|
|
||||||
|
use Ulmus\Query;
|
||||||
|
|
||||||
|
trait JsonConditionTrait
|
||||||
|
{
|
||||||
|
public function whereJsonArray(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, $condition = Query\Where::CONDITION_AND) : self
|
||||||
|
{
|
||||||
|
if ( null === $this->queryBuilder->getFragment(Query\From::class) ) {
|
||||||
|
$this->from($this->entityResolver->tableName(), $this->alias, $this->entityResolver->schemaName());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->queryBuilder->whereJsonArray($field, $value, $operator, $condition);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user