diff --git a/docs/01-load.md b/docs/01-load.md index e69de29..6c61724 100644 --- a/docs/01-load.md +++ b/docs/01-load.md @@ -0,0 +1,7 @@ +# Search Request + +Ulmus comes with a simple search request processor which allows both flexibility and simplicity. + +## Quick start + +Creating a simple user exemple entity: diff --git a/docs/50-search-request.md b/docs/50-search-request.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php new file mode 100644 index 0000000..99aa9d0 --- /dev/null +++ b/src/Adapter/DefaultAdapterTrait.php @@ -0,0 +1,121 @@ + "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, + ]); + } +} diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index 160367f..40055fa 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -230,6 +230,6 @@ class MsSQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { public function queryBuilderClass() : string { - return QueryBuilder\MssqlQueryBuilder::class; + return QueryBuilder\SqlQueryBuilder\MssqlQueryBuilder::class; } } diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index 8d3e8df..53d440b 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -2,15 +2,11 @@ namespace Ulmus\Adapter; -use Ulmus\Entity\InformationSchema\Table; use Ulmus\Migration\MigrateInterface; -use Ulmus\QueryBuilder; -use Ulmus\Repository; +use Ulmus\QueryBuilder\Sql; use Ulmus\Common\PdoObject; use Ulmus\Exception\AdapterConfigurationException; -use Ulmus\Ulmus; -use Ulmus\Migration\FieldDefinition; class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { use SqlAdapterTrait; @@ -157,4 +153,10 @@ class MySQL implements AdapterInterface, MigrateInterface, SqlAdapterInterface { { return "InnoDB"; } + + public function queryBuilderClass() : string + { + return Sql\MysqlQueryBuilder::class; + } + } diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index d8f94a8..53ac497 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -5,10 +5,9 @@ namespace Ulmus\Adapter; use Ulmus\Common\PdoObject; use Ulmus\ConnectionAdapter; -use Ulmus\Entity\Sqlite\Table; -use Ulmus\Exception\AdapterConfigurationException; +use Ulmus\Entity; use Ulmus\Migration\FieldDefinition; -use Ulmus\{Migration\MigrateInterface, Repository, QueryBuilder, Ulmus}; +use Ulmus\{Migration\MigrateInterface, Repository, QueryBuilder}; class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface { use SqlAdapterTrait; @@ -90,9 +89,9 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface public function schemaTable(ConnectionAdapter $adapter, string $databaseName, string $tableName) : null|object { - return Table::repository(Repository::DEFAULT_ALIAS, $adapter) + return Entity\Sqlite\Table::repository(Repository::DEFAULT_ALIAS, $adapter) ->select(\Ulmus\Common\Sql::raw('this.*')) - ->loadOneFromField(Table::field('tableName'), $tableName); + ->loadOneFromField(Entity\Sqlite\Table::field('tableName'), $tableName); } public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string @@ -150,7 +149,7 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface public function queryBuilderClass() : string { - return QueryBuilder\SqliteQueryBuilder::class; + return QueryBuilder\Sql\SqliteQueryBuilder::class; } public function exportFunctions(PdoObject $pdo) : void @@ -192,7 +191,7 @@ class SQLite implements AdapterInterface, MigrateInterface, SqlAdapterInterface public static function registerPragma(PdoObject $pdo, array $pragmaList) : void { - $builder = new QueryBuilder\SqliteQueryBuilder(); + $builder = new QueryBuilder\Sql\SqliteQueryBuilder(); foreach($pragmaList as $pragma) { list($key, $value) = explode('=', $pragma) + [ null, null ]; diff --git a/src/Adapter/SqlAdapterTrait.php b/src/Adapter/SqlAdapterTrait.php index bf2a2b5..04f7288 100644 --- a/src/Adapter/SqlAdapterTrait.php +++ b/src/Adapter/SqlAdapterTrait.php @@ -7,7 +7,7 @@ use Ulmus\{Common\Sql, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, - QueryBuilder, + QueryBuilder\SqlQueryBuilder, Ulmus}; trait SqlAdapterTrait @@ -19,7 +19,7 @@ trait SqlAdapterTrait public function queryBuilderClass() : string { - return QueryBuilder::class; + return SqlQueryBuilder::class; } public function tableSyntax() : array diff --git a/src/EntityTrait.php b/src/EntityTrait.php index db86bc5..6cd0772 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -4,19 +4,16 @@ namespace Ulmus; use Notes\Attribute\Ignore; use Psr\Http\Message\ServerRequestInterface; -use Ulmus\{Common\EntityResolver, - Common\EntityField, - Entity\EntityInterface, - Query\QueryBuilderInterface, - SearchRequest\SearchRequestInterface, - SearchRequest\SearchRequestPaginationTrait}; +use Ulmus\{Common\EntityResolver, Common\EntityField, Entity\EntityInterface, Query\QueryBuilderInterface}; +use Ulmus\SearchRequest\{Attribute\SearchParameter, + SearchMethodEnum, + SearchRequestInterface, + SearchRequestFromRequestTrait, + SearchRequestPaginationTrait}; trait EntityTrait { use EventTrait; - #[Ignore] - public string $loadedFromAdapter; - #[Ignore] protected bool $entityStrictFieldsDeclaration = false; @@ -327,6 +324,7 @@ trait EntityTrait { #[Ignore] public static function field($name, null|string|false $alias = Repository::DEFAULT_ALIAS) : EntityField { + $default = ( $alias === false ? '' : Repository::DEFAULT_ALIAS ); # bw compatibility, to be deprecated return new EntityField(static::class, $name, $alias ? Ulmus::repository(static::class)->adapter->adapter()->escapeIdentifier($alias, Adapter\AdapterInterface::IDENTIFIER_FIELD) : $default, Ulmus::resolveEntity(static::class)); @@ -343,47 +341,17 @@ trait EntityTrait { #[Ignore] public static function searchRequest(...$arguments) : SearchRequestInterface { - return new class() implements SearchRequestInterface, \JsonSerializable - { - use SearchRequestPaginationTrait; + return new #[SearchRequest\Attribute\SearchRequestParameter(self::class)] class(... $arguments) extends SearchRequest\SearchRequest { + # Define searchable properties here, some ex: - public function fromRequest(ServerRequestInterface $request) : self - { - $get = new \ArrayObject(array_filter($request->getQueryParams(), function($i) { return $i !== ""; })); + # #[SearchParameter(method: SearchMethodEnum::Where)] + # public ? string $username = null; - $this->page = $get->offsetExists('page') ? $get['page'] : 1; - $this->limit = 100; + # #[SearchParameter(method: SearchMethodEnum::Where, toggle: true)] + # public ? string $hidden = null; - return $this; - } - - public function filter(Repository $repository) : Repository - { - return $repository; - } - - public function wheres() : iterable - { - return array_filter([ - ], fn($i) => ! is_null($i) ) + [ ]; - } - - public function likes(): iterable - { - return array_filter([ - ], fn($i) => ! is_null($i) ) + []; - } - - public function groups(): iterable - { - return []; - } - - public function orders(): iterable - { - return array_filter([ - ], fn($e) => ! is_null($e) && $e !== "" ); - } + # #[SearchParameter(method: SearchMethodEnum::Like)] + # public ? string $word = null; }; } } diff --git a/src/Query/Having.php b/src/Query/Having.php index 96552bd..d584501 100644 --- a/src/Query/Having.php +++ b/src/Query/Having.php @@ -2,7 +2,7 @@ namespace Ulmus\Query; -use Ulmus\QueryBuilder; +use Ulmus\MysqlQueryBuilder; use Ulmus\Common\EntityField, Ulmus\Common\Sql; diff --git a/src/Query/Join.php b/src/Query/Join.php index c5ccb11..18b7c7b 100644 --- a/src/Query/Join.php +++ b/src/Query/Join.php @@ -2,7 +2,7 @@ namespace Ulmus\Query; -use Ulmus\QueryBuilder; +use Ulmus\MysqlQueryBuilder; use Ulmus\Repository\ConditionTrait; class Join extends Fragment diff --git a/src/Query/Set.php b/src/Query/Set.php index b97e023..69d2379 100644 --- a/src/Query/Set.php +++ b/src/Query/Set.php @@ -2,7 +2,7 @@ namespace Ulmus\Query; -use Ulmus\QueryBuilder; +use Ulmus\MysqlQueryBuilder; class Set extends Fragment { diff --git a/src/Query/Show.php b/src/Query/Show.php index dbeb275..01edf25 100644 --- a/src/Query/Show.php +++ b/src/Query/Show.php @@ -10,10 +10,14 @@ class Show extends Fragment { const SQL_TOKEN_FROM = "FROM"; + const SQL_TOKEN_IN = "IN"; + const SQL_SHOW_DATABASES = "DATABASES"; const SQL_SHOW_TABLES = "TABLES"; + const SQL_SHOW_COLUMNS = "COLUMNS"; + public string $show; public string $from; diff --git a/src/Query/Values.php b/src/Query/Values.php index be33dea..215b28a 100644 --- a/src/Query/Values.php +++ b/src/Query/Values.php @@ -2,7 +2,7 @@ namespace Ulmus\Query; -use Ulmus\QueryBuilder; +use Ulmus\MysqlQueryBuilder; class Values extends Fragment { @@ -12,9 +12,9 @@ class Values extends Fragment { public array $rows; - public QueryBuilder $queryBuilder; + public MysqlQueryBuilder $queryBuilder; - public function __construct(QueryBuilder $queryBuilder) + public function __construct(MysqlQueryBuilder $queryBuilder) { $this->queryBuilder = $queryBuilder; } diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 138ce2c..4d37678 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -2,541 +2,7 @@ namespace Ulmus; -use Ulmus\Query\QueryBuilderInterface; - -class QueryBuilder implements Query\QueryBuilderInterface +class QueryBuilder extends QueryBuilder\MysqlQueryBuilder { - public Query\Where $where; - - public Query\Having $having; - - public QueryBuilderInterface $parent; - - /** - * Those are the parameters we are going to bind to PDO. - */ - public array $parameters = []; - - /** - * - * Those values are to be inserted or updated - */ - public array $values = []; - - public string $whereConditionOperator = Query\Where::CONDITION_AND; - - public string $havingConditionOperator = Query\Where::CONDITION_AND; - - protected int $parameterIndex = 0; - - protected array $queryStack = []; - - public function __clone() - { - if ($this->where ?? false) { - #$this->where = clone $this->where; - #$this->where->queryBuilder = $this; - } - - if ($this->having ?? false) { - #$this->having = clone $this->having; - #$this->having->queryBuiler = $this; - } - - if ($this->parent ?? false) { - #$this->parent = clone $this->parent; - } - } - - public function select(string|\Stringable|array $field, bool $distinct = false) : self - { - if ( null !== ( $select = $this->getFragment(Query\Select::class) ) ) { - $select->add($field); - } - else { - $select = new Query\Select(); - $select->set($field); - $this->push($select); - } - - $select->distinct = $distinct; - - return $this; - } - - public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null, bool $replace = false) : self - { - if ( null === $this->getFragment(Query\Insert::class) ) { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - $insert = new Query\Insert(); - $this->push($insert); - - $insert->replace = $replace; - $insert->fieldlist = $fieldlist; - $insert->alias = $alias; - $insert->table = $table; - } - - return $this; - } - - public function values(array $dataset) : self - { - if ( null === ( $values = $this->getFragment(Query\Values::class) ) ) { - $values = new Query\Values($this); - $this->push($values); - } - - $values->add($dataset); - - return $this; - } - - public function update(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self - { - if ( ! $this->getFragment(Query\Update::class) ) { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - $update = new Query\Update(); - $this->push($update); - - $update->alias = $alias; - $update->table = $table; - } - - return $this; - } - - public function set(array $dataset, ? array $escapedFields = null) : self - { - if ( null === ( $set = $this->getFragment(Query\Set::class) ) ) { - $set = new Query\Set($this); - $this->push($set); - } - - $set->set($dataset, $escapedFields); - - return $this; - } - - public function delete() : self - { - if ( ! $this->getFragment(Query\Delete::class) ) { - $this->push(new Query\Delete()); - } - - return $this; - } - - public function from(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self - { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - if ( null !== ( $from = $this->getFragment(Query\From::class) ) ) { - $from->add($alias ? [ $alias => $table ] : $table); - } - else { - $from = new Query\From($this); - $this->push($from); - - $from->set($alias ? [ $alias => $table ] : [ $table ]); - } - - return $this; - } - - public function open(string $condition = Query\Where::CONDITION_AND) : self - { - if ( null !== ($this->where ?? null) ) { - $this->where->conditionList[] = $new = new Query\Where($this, $condition); - $this->where = $new; - } - else { - $this->where = new Query\Where($this, $condition); - $this->push($this->where); - $this->where->conditionList[] = $new = new Query\Where($this, $condition); - $this->where = $new; - } - - return $this; - } - - public function close() : self - { - if ( null !== ($this->where ?? null) && $this->where->parent ) { - - # if an enclosure was opened, and nothing done, we must remove the unused node - if ( empty($this->where->conditionList) && (count($this->where->parent->conditionList) === 1) ) { - unset($this->where->parent->conditionList); - } - - $this->where = $this->where->parent; - } - - return $this; - } - - public function where(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self - { - # Empty IN case - # if ( [] === $value ) { - # return $this; - # } - - if ( $this->where ?? false ) { - $where = $this->where; - } - elseif ( null === ( $where = $this->getFragment(Query\Where::class) ) ) { - $this->where = $where = new Query\Where($this); - $this->push($where); - } - - $this->whereConditionOperator = $operator; - - $where->add($field, $value, $operator, $condition, $not); - - return $this; - } - - public function notWhere(string|\Stringable $field, mixed $value, string $operator = Query\Where::CONDITION_AND) : self - { - return $this->where($field, $value, $operator, true); - } - - public function having(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self - { - if ( $this->having ?? false ) { - $having = $this->having; - } - elseif ( null === ( $having = $this->getFragment(Query\Having::class) ) ) { - $this->having = $having = new Query\Having($this); - $this->push($having); - } - - $this->havingConditionOperator = $operator; - $having->add($field, $value, $operator, $condition, $not); - - return $this; - } - - public function limit(int $value) : self - { - if ( null === $limit = $this->getFragment(Query\Limit::class) ) { - $limit = new Query\Limit(); - $this->push($limit); - } - - $limit->set($value); - - if ($value === 0) { - $this->removeFragment(Query\Limit::class); - } - - return $this; - } - - public function offset(int $value) : self - { - if ( null === $offset = $this->getFragment(Query\Offset::class) ) { - $offset = new Query\Offset(); - $this->push($offset); - - # A limit is required to match an offset - if ( null === $limit = $this->getFragment(Query\Limit::class) ) { - $this->limit(\PHP_INT_MAX); - } - } - - $offset->set($value); - - return $this; - } - - public function orderBy(string|\Stringable $field, ? string $direction = null) : self - { - if ( null === $orderBy = $this->getFragment(Query\OrderBy::class) ) { - $orderBy = new Query\OrderBy(); - $this->push($orderBy); - } - - $orderBy->add($field, $direction); - - return $this; - } - - public function groupBy(string|object $field, ? string $direction = null) : self - { - if ( null === $groupBy = $this->getFragment(Query\GroupBy::class) ) { - $groupBy = new Query\GroupBy(); - $this->push($groupBy); - } - - $groupBy->add($field, $direction); - - return $this; - } - - public function join(string $type, /*string | QueryBuilder*/ $table, mixed $field, mixed $value, bool $outer = false, ? string $alias = null) : self - { - $this->withJoin(...func_get_args()); - - return $this; - } - - public function withJoin(string $type, $table, mixed $field, mixed $value, bool $outer = false, ? string $alias = null) : Query\Join - { - $join = new Query\Join($this); - - $this->push($join); - - $join->set($type, $table, $field, $value); - - $join->outer = $outer; - - $join->alias = $alias; - - $join->joinOrder = $this->nextJoinOrder(); - - return $join; - } - - public function showDatabases() : self - { - $show = new Query\Show(); - - $this->push($show); - - $show->set(Query\Show::SQL_SHOW_DATABASES); - - return $this; - } - - public function showTables(? string $database = null) : self - { - $show = new Query\Show(); - - $this->push($show); - - $show->set(Query\Show::SQL_SHOW_TABLES, $database); - - return $this; - } - - public function showColumns(string $table) : self - { - $show = new Query\Show(); - - $this->push($show); - - $show->set(Query\Show::SQL_SHOW_COLUMNS, $table); - - return $this; - } - - public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self - { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - $truncate = new Query\Truncate($this); - - $this->push($truncate); - - $truncate->set($table); - - return $this; - } - - public function create(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self - { - if ( null === $this->getFragment(Query\Create::class) ) { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - $create = new Query\Create($adapter); - $this->push($create); - - $create->fieldList = $fieldlist; - $create->table = $table; - } - else { - throw new \Exception("A create SQL fragment was already found within the query builder"); - } - - return $this; - } - - public function alter(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self - { - if ( null === $this->getFragment(Query\Alter::class) ) { - if ( $schema ) { - $table = "$schema.$table"; - } - - if ( $database ) { - $table = "$database.$table"; - } - - $alter = new Query\Alter($adapter); - $this->push($alter); - - $alter->fieldList = $fieldlist; - $alter->table = $table; - } - else { - throw new \Exception("A create SQL fragment was already found within the query builder"); - } - - return $this; - } - - public function engine(string $value) : self - { - if ( null === $engine = $this->getFragment(Query\Engine::class) ) { - $engine = new Query\Engine(); - $this->push($engine); - } - - $engine->engine = $value; - - return $this; - } - - public function push(Query\Fragment $queryFragment) : self - { - $this->queryStack[] = $queryFragment; - - return $this; - } - - public function pull(Query\Fragment $queryFragment) : self - { - return array_shift($this->queryStack); - } - - public function render(bool $skipToken = false) /* : mixed */ - { - $sql = []; - - usort($this->queryStack, function($q1, $q2) { - return (float) $q1->order() <=> (float) $q2->order(); - }); - - foreach($this->queryStack as $fragment) { - $sql[] = $fragment->render($skipToken); - } - - return implode(" ", $sql); - } - - public function reset() : void - { - $this->parameters = $this->values = $this->queryStack = []; - $this->whereConditionOperator = Query\Where::CONDITION_AND; - $this->havingConditionOperator = Query\Where::CONDITION_AND; - $this->parameterIndex = 0; - - unset($this->where, $this->having); - } - - public function getFragment(string $class, int $index = 0) : ? Query\Fragment - { - foreach($this->queryStack as $item) { - if ( is_a($item, $class, true) ) { - if ( $index-- === 0 ) { - return $item; - } - } - } - - return null; - } - - public function removeFragment(Query\Fragment|array|\Stringable|string $fragment) : void - { - is_object($fragment) && $fragment = get_class($fragment); - - foreach($this->queryStack as $key => $item) { - if ( get_class($item) === $fragment ) { - unset($this->queryStack[$key]); - } - } - - if ( $fragment === Query\Where::class ) { - unset($this->where); - } - elseif ( $fragment === Query\Having::class ) { - unset($this->having); - } - } - - public function getFragments(Query\Fragment|array|\Stringable|string $fragment = null) : array - { - return $fragment !== null ? array_filter($this->queryStack, fn($e) => is_a($e, $fragment, true) ) : $this->queryStack; - } - - public function __toString() : string - { - return $this->render(); - } - - public function addParameter(mixed $value, string $key = null) : string - { - if ( $this->parent ?? false ) { - return $this->parent->addParameter($value, $key); - } - - if ( $key === null ) { - $key = ":p" . $this->parameterIndex++; - } - - $this->parameters[$key] = $value; - - return $key; - } - - public function addValues(array $values) : void - { - $this->values = $values; - } - - protected function nextJoinOrder() : float - { - $next = 0; - - foreach($this->getFragments(Query\Join::class) as $join) { - $next = max($next, $join->joinOrder - Query\Join::ORDER_VALUE); - } - - return $next + 1; - } -} + # Backward compatibility defaulting on MySQL/MariaDB query builder +} \ No newline at end of file diff --git a/src/QueryBuilder/MssqlQueryBuilder.php b/src/QueryBuilder/Sql/MssqlQueryBuilder.php similarity index 76% rename from src/QueryBuilder/MssqlQueryBuilder.php rename to src/QueryBuilder/Sql/MssqlQueryBuilder.php index bd8e4f7..7f08fa2 100644 --- a/src/QueryBuilder/MssqlQueryBuilder.php +++ b/src/QueryBuilder/Sql/MssqlQueryBuilder.php @@ -1,10 +1,12 @@ where ?? false) { + #$this->where = clone $this->where; + #$this->where->queryBuilder = $this; + } + + if ($this->having ?? false) { + #$this->having = clone $this->having; + #$this->having->queryBuiler = $this; + } + + if ($this->parent ?? false) { + #$this->parent = clone $this->parent; + } + } + + public function select(string|\Stringable|array $field, bool $distinct = false) : self + { + if ( null !== ( $select = $this->getFragment(Query\Select::class) ) ) { + $select->add($field); + } + else { + $select = new Query\Select(); + $select->set($field); + $this->push($select); + } + + $select->distinct = $distinct; + + return $this; + } + + public function insert(array $fieldlist, string $table, ? string $alias = null, ? string $database = null, ? string $schema = null, bool $replace = false) : self + { + if ( null === $this->getFragment(Query\Insert::class) ) { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + $insert = new Query\Insert(); + $this->push($insert); + + $insert->replace = $replace; + $insert->fieldlist = $fieldlist; + $insert->alias = $alias; + $insert->table = $table; + } + + return $this; + } + + public function values(array $dataset) : self + { + if ( null === ( $values = $this->getFragment(Query\Values::class) ) ) { + $values = new Query\Values($this); + $this->push($values); + } + + $values->add($dataset); + + return $this; + } + + public function update(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self + { + if ( ! $this->getFragment(Query\Update::class) ) { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + $update = new Query\Update(); + $this->push($update); + + $update->alias = $alias; + $update->table = $table; + } + + return $this; + } + + public function set(array $dataset, ? array $escapedFields = null) : self + { + if ( null === ( $set = $this->getFragment(Query\Set::class) ) ) { + $set = new Query\Set($this); + $this->push($set); + } + + $set->set($dataset, $escapedFields); + + return $this; + } + + public function delete() : self + { + if ( ! $this->getFragment(Query\Delete::class) ) { + $this->push(new Query\Delete()); + } + + return $this; + } + + public function from(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self + { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + if ( null !== ( $from = $this->getFragment(Query\From::class) ) ) { + $from->add($alias ? [ $alias => $table ] : $table); + } + else { + $from = new Query\From($this); + $this->push($from); + + $from->set($alias ? [ $alias => $table ] : [ $table ]); + } + + return $this; + } + + public function open(string $condition = Query\Where::CONDITION_AND) : self + { + if ( null !== ($this->where ?? null) ) { + $this->where->conditionList[] = $new = new Query\Where($this, $condition); + $this->where = $new; + } + else { + $this->where = new Query\Where($this, $condition); + $this->push($this->where); + $this->where->conditionList[] = $new = new Query\Where($this, $condition); + $this->where = $new; + } + + return $this; + } + + public function close() : self + { + if ( null !== ($this->where ?? null) && $this->where->parent ) { + + # if an enclosure was opened, and nothing done, we must remove the unused node + if ( empty($this->where->conditionList) && (count($this->where->parent->conditionList) === 1) ) { + unset($this->where->parent->conditionList); + } + + $this->where = $this->where->parent; + } + + return $this; + } + + public function where(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self + { + # Empty IN case + # if ( [] === $value ) { + # return $this; + # } + + if ( $this->where ?? false ) { + $where = $this->where; + } + elseif ( null === ( $where = $this->getFragment(Query\Where::class) ) ) { + $this->where = $where = new Query\Where($this); + $this->push($where); + } + + $this->whereConditionOperator = $operator; + + $where->add($field, $value, $operator, $condition, $not); + + return $this; + } + + public function notWhere(string|\Stringable $field, mixed $value, string $operator = Query\Where::CONDITION_AND) : self + { + return $this->where($field, $value, $operator, true); + } + + public function having(string|\Stringable $field, mixed $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self + { + if ( $this->having ?? false ) { + $having = $this->having; + } + elseif ( null === ( $having = $this->getFragment(Query\Having::class) ) ) { + $this->having = $having = new Query\Having($this); + $this->push($having); + } + + $this->havingConditionOperator = $operator; + $having->add($field, $value, $operator, $condition, $not); + + return $this; + } + + public function limit(int $value) : self + { + if ( null === $limit = $this->getFragment(Query\Limit::class) ) { + $limit = new Query\Limit(); + $this->push($limit); + } + + $limit->set($value); + + if ($value === 0) { + $this->removeFragment(Query\Limit::class); + } + + return $this; + } + + public function offset(int $value) : self + { + if ( null === $offset = $this->getFragment(Query\Offset::class) ) { + $offset = new Query\Offset(); + $this->push($offset); + + # A limit is required to match an offset + if ( null === $limit = $this->getFragment(Query\Limit::class) ) { + $this->limit(\PHP_INT_MAX); + } + } + + $offset->set($value); + + return $this; + } + + public function orderBy(string|\Stringable $field, ? string $direction = null) : self + { + if ( null === $orderBy = $this->getFragment(Query\OrderBy::class) ) { + $orderBy = new Query\OrderBy(); + $this->push($orderBy); + } + + $orderBy->add($field, $direction); + + return $this; + } + + public function groupBy(string|object $field, ? string $direction = null) : self + { + if ( null === $groupBy = $this->getFragment(Query\GroupBy::class) ) { + $groupBy = new Query\GroupBy(); + $this->push($groupBy); + } + + $groupBy->add($field, $direction); + + return $this; + } + + public function join(string $type, /*string | QueryBuilder*/ $table, mixed $field, mixed $value, bool $outer = false, ? string $alias = null) : self + { + $this->withJoin(...func_get_args()); + + return $this; + } + + public function withJoin(string $type, $table, mixed $field, mixed $value, bool $outer = false, ? string $alias = null) : Query\Join + { + $join = new Query\Join($this); + + $this->push($join); + + $join->set($type, $table, $field, $value); + + $join->outer = $outer; + + $join->alias = $alias; + + $join->joinOrder = $this->nextJoinOrder(); + + return $join; + } + + public function showDatabases() : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_DATABASES); + + return $this; + } + + public function showTables(? string $database = null) : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_TABLES, $database); + + return $this; + } + + public function showColumns(string $table) : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_COLUMNS, $table); + + return $this; + } + + public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self + { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + $truncate = new Query\Truncate($this); + + $this->push($truncate); + + $truncate->set($table); + + return $this; + } + + public function create(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self + { + if ( null === $this->getFragment(Query\Create::class) ) { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + $create = new Query\Create($adapter); + $this->push($create); + + $create->fieldList = $fieldlist; + $create->table = $table; + } + else { + throw new \Exception("A create SQL fragment was already found within the query builder"); + } + + return $this; + } + + public function alter(Adapter\AdapterInterface $adapter, array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self + { + if ( null === $this->getFragment(Query\Alter::class) ) { + if ( $schema ) { + $table = "$schema.$table"; + } + + if ( $database ) { + $table = "$database.$table"; + } + + $alter = new Query\Alter($adapter); + $this->push($alter); + + $alter->fieldList = $fieldlist; + $alter->table = $table; + } + else { + throw new \Exception("A create SQL fragment was already found within the query builder"); + } + + return $this; + } + + public function engine(string $value) : self + { + if ( null === $engine = $this->getFragment(Query\Engine::class) ) { + $engine = new Query\Engine(); + $this->push($engine); + } + + $engine->engine = $value; + + return $this; + } + + public function push(Query\Fragment $queryFragment) : self + { + $this->queryStack[] = $queryFragment; + + return $this; + } + + public function pull(Query\Fragment $queryFragment) : self + { + return array_shift($this->queryStack); + } + + public function render(bool $skipToken = false) /* : mixed */ + { + $sql = []; + + usort($this->queryStack, function($q1, $q2) { + return (float) $q1->order() <=> (float) $q2->order(); + }); + + foreach($this->queryStack as $fragment) { + $sql[] = $fragment->render($skipToken); + } + + return implode(" ", $sql); + } + + public function reset() : void + { + $this->parameters = $this->values = $this->queryStack = []; + $this->whereConditionOperator = Query\Where::CONDITION_AND; + $this->havingConditionOperator = Query\Where::CONDITION_AND; + $this->parameterIndex = 0; + + unset($this->where, $this->having); + } + + public function getFragment(string $class, int $index = 0) : ? Query\Fragment + { + foreach($this->queryStack as $item) { + if ( is_a($item, $class, true) ) { + if ( $index-- === 0 ) { + return $item; + } + } + } + + return null; + } + + public function removeFragment(Query\Fragment|array|\Stringable|string $fragment) : void + { + is_object($fragment) && $fragment = get_class($fragment); + + foreach($this->queryStack as $key => $item) { + if ( get_class($item) === $fragment ) { + unset($this->queryStack[$key]); + } + } + + if ( $fragment === Query\Where::class ) { + unset($this->where); + } + elseif ( $fragment === Query\Having::class ) { + unset($this->having); + } + } + + public function getFragments(Query\Fragment|array|\Stringable|string $fragment = null) : array + { + return $fragment !== null ? array_filter($this->queryStack, fn($e) => is_a($e, $fragment, true) ) : $this->queryStack; + } + + public function __toString() : string + { + return $this->render(); + } + + public function addParameter(mixed $value, string $key = null) : string + { + if ( $this->parent ?? false ) { + return $this->parent->addParameter($value, $key); + } + + if ( $key === null ) { + $key = ":p" . $this->parameterIndex++; + } + + $this->parameters[$key] = $value; + + return $key; + } + + public function addValues(array $values) : void + { + $this->values = $values; + } + + protected function nextJoinOrder() : float + { + $next = 0; + + foreach($this->getFragments(Query\Join::class) as $join) { + $next = max($next, $join->joinOrder - Query\Join::ORDER_VALUE); + } + + return $next + 1; + } +} diff --git a/src/QueryBuilder/SqliteQueryBuilder.php b/src/QueryBuilder/Sql/SqliteQueryBuilder.php similarity index 80% rename from src/QueryBuilder/SqliteQueryBuilder.php rename to src/QueryBuilder/Sql/SqliteQueryBuilder.php index 6c88e5a..f989b0c 100644 --- a/src/QueryBuilder/SqliteQueryBuilder.php +++ b/src/QueryBuilder/Sql/SqliteQueryBuilder.php @@ -1,12 +1,12 @@ pragma('table_info', $table, true); diff --git a/src/QueryBuilder/SqlQueryBuilder.php b/src/QueryBuilder/SqlQueryBuilder.php new file mode 100644 index 0000000..987e4b5 --- /dev/null +++ b/src/QueryBuilder/SqlQueryBuilder.php @@ -0,0 +1,42 @@ + Extract from MysqlQueryBuilder to build an ISO/IEC 9075:2023 compatible layer for a basic SQL QueryBuilder +class SqlQueryBuilder implements Query\QueryBuilderInterface +{ + + public function push(Fragment $queryFragment): Query\QueryBuilderInterface + { + // TODO: Implement push() method. + } + + public function pull(Fragment $queryFragment): Query\QueryBuilderInterface + { + // TODO: Implement pull() method. + } + + public function render(bool $skipToken = false) + { + // TODO: Implement render() method. + } + + public function reset(): void + { + // TODO: Implement reset() method. + } + + public function getFragment(string $class, int $index = 0): ?Fragment + { + // TODO: Implement getFragment() method. + } + + public function removeFragment(Fragment|\Stringable|array|string $fragment): void + { + // TODO: Implement removeFragment() method. + } +} \ No newline at end of file diff --git a/src/Repository/MysqlRepository.php b/src/Repository/MysqlRepository.php index b12d87d..970d457 100644 --- a/src/Repository/MysqlRepository.php +++ b/src/Repository/MysqlRepository.php @@ -2,7 +2,7 @@ namespace Ulmus\Repository; -use Ulmus\{Common\EntityResolver, ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus}; +use Ulmus\{Common\EntityResolver, ConnectionAdapter, MysqlQueryBuilder, Repository, Query, Ulmus}; class MysqlRepository extends Repository { diff --git a/src/Repository/SqliteRepository.php b/src/Repository/SqliteRepository.php index ca21f5e..9e08f89 100644 --- a/src/Repository/SqliteRepository.php +++ b/src/Repository/SqliteRepository.php @@ -2,7 +2,7 @@ namespace Ulmus\Repository; -use Ulmus\{ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus, Entity}; +use Ulmus\{ConnectionAdapter, MysqlQueryBuilder, Repository, Query, Ulmus, Entity}; class SqliteRepository extends Repository { diff --git a/src/SearchRequest/Attribute/SearchEqual.php b/src/SearchRequest/Attribute/SearchEqual.php new file mode 100644 index 0000000..110598b --- /dev/null +++ b/src/SearchRequest/Attribute/SearchEqual.php @@ -0,0 +1,6 @@ +wheres + [ + ], fn($i) => ! is_null($i) ) + [ ]; + } + + public function likes(): iterable + { + return array_filter($this->likes + [ + ], fn($i) => ! is_null($i) ) + []; + } + + public function groups(): iterable + { + return array_filter($this->groups + [ + ], fn($e) => ! is_null($e) && $e !== "" ); + } + + public function orders(): iterable + { + return array_filter($this->orders + [ + ], fn($e) => ! is_null($e) && $e !== "" ); + } + +} \ No newline at end of file diff --git a/src/SearchRequest/SearchRequestFromRequestTrait.php b/src/SearchRequest/SearchRequestFromRequestTrait.php new file mode 100644 index 0000000..811439a --- /dev/null +++ b/src/SearchRequest/SearchRequestFromRequestTrait.php @@ -0,0 +1,83 @@ +getQueryParams(), function($i) { return $i !== ""; })); + + $this->page = $get->offsetExists('page') ? $get['page'] : 1; + + $classReflection = new \ReflectionClass($this); + + foreach($classReflection->getProperties() as $property) { + + foreach($property->getAttributes() as $attributeReflection) { + $attribute = $attributeReflection->newInstance(); + + # We can't simply pass this class as first arguments of getAttributes() since it do not check for inheritance + if (! $attribute instanceof Attribute\SearchParameter) { + continue; + } + + $propertyName = $property->getName(); + $fieldName = $attribute->field ?? $property->getName(); + $queryParamName = $attribute->parameter ?? $property->getName(); + + if ($attribute->toggle) { + $this->$propertyName = $get->offsetExists('rental'); + } else { + $this->$propertyName = $get->offsetExists($queryParamName) ? $get[$queryParamName] : null; + } + + $field = (string) $fieldName; + + switch ($attribute->method) { + case SearchMethodEnum::Where: + $this->wheres[$field] = $this->$propertyName; + break; + + case SearchMethodEnum::Like: + $this->likes[$field] = "%{$this->$propertyName}%"; + break; + + case SearchMethodEnum::LikeLeft: + $this->likes[$field] = "%{$this->$propertyName}"; + break; + + case SearchMethodEnum::LikeRight: + $this->likes[$field] = "{$this->$propertyName}%"; + break; + + case SearchMethodEnum::OrderByAsc: + $this->orders[$field] = "ASC"; + break; + + case SearchMethodEnum::OrderByDesc: + $this->orders[$field] = "DESC"; + break; + + case SearchMethodEnum::GroupBy: + $this->groups[$field] = "DESC"; + break; + } + } + } + + return $this; + } +} \ No newline at end of file