From e75dbd69d294fff9d62ac5a02a15fef778eb5292 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Sun, 25 Oct 2020 19:11:34 +0100 Subject: [PATCH 01/16] implement public query interface --- docs/reference.md | 302 +++++++++++++++++-- src/Dbal/Dbal.php | 12 +- src/EntityFetcher.php | 68 ++--- src/EntityManager.php | 13 + src/QueryBuilder/QueryBuilder.php | 85 ++++++ tests/Dbal/BulkInsertTest.php | 3 +- tests/Dbal/Sqlite/InsertTest.php | 3 +- tests/EntityManager/DataModificationTest.php | 9 +- tests/EntityManager/EntityFetcherTest.php | 8 +- tests/EntityManager/MappingTest.php | 3 +- tests/Relation/ManyToManyTest.php | 6 +- 11 files changed, 419 insertions(+), 93 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 18186a8..3890961 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3397,6 +3397,7 @@ Supported: * [orWhereNotIn](#ormentityfetcherorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormentityfetcherparenthesis) Alias for andParenthesis * [rightJoin](#ormentityfetcherrightjoin) Right (outer) join $tableName with $options +* [setFetchMode](#ormentityfetchersetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormentityfetchersetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormentityfetchertoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormentityfetchertranslatecolumn) Translate attribute names in an expression to their column names @@ -3778,7 +3779,7 @@ Builds the statement from current where conditions, joins, columns and so on. #### ORM\EntityFetcher::getStatement ```php -private function getStatement(): \PDOStatement|boolean +protected function getStatement(): \PDOStatement|boolean ``` ##### Query database and return result @@ -3788,7 +3789,7 @@ Queries the database with current query and returns the resulted PDOStatement. If query failed it returns false. It also stores this failed result and to change the query afterwards will not change the result. -**Visibility:** this method is **private**. +**Visibility:** this method is **protected**.
**Returns**: this method returns **\PDOStatement|boolean**
@@ -4230,6 +4231,33 @@ can be set to true. +#### ORM\EntityFetcher::setFetchMode + +```php +public function setFetchMode( + integer $mode, null $classNameObject = null, array $ctorarfg = array() +): $this +``` + +##### Proxy to PDOStatement::setFetchMode() + +Please note that this will execute the query - further modifications will not have any effect. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$mode` | **integer** | | +| `$classNameObject` | **null** | | +| `$ctorarfg` | **array** | | + + + #### ORM\EntityFetcher::setQuery ```php @@ -4480,7 +4508,7 @@ Supported: #### Methods * [__construct](#ormtestingentityfetchermock__construct) Constructor -* [all](#ormtestingentityfetchermockall) Fetch an array of entities +* [all](#ormtestingentityfetchermockall) Get all rows from the query result * [andParenthesis](#ormtestingentityfetchermockandparenthesis) Add a parenthesis with AND * [andWhere](#ormtestingentityfetchermockandwhere) Add a where condition with AND. * [close](#ormtestingentityfetchermockclose) Close parenthesis @@ -4513,6 +4541,7 @@ Supported: * [orWhereNotIn](#ormtestingentityfetchermockorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormtestingentityfetchermockparenthesis) Alias for andParenthesis * [rightJoin](#ormtestingentityfetchermockrightjoin) Right (outer) join $tableName with $options +* [setFetchMode](#ormtestingentityfetchermocksetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormtestingentityfetchermocksetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormtestingentityfetchermocktoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormtestingentityfetchermocktranslatecolumn) Translate attribute names in an expression to their column names @@ -4549,24 +4578,21 @@ Create a parenthesis inside another parenthesis or a query. #### ORM\Testing\EntityFetcherMock::all ```php -public function all( integer $limit ): array<\ORM\Entity> +public function all(): mixed|null ``` -##### Fetch an array of entities +##### Get all rows from the query result -When no $limit is set it fetches all entities in result set. +Please note that this will execute the query - further modifications will not have any effect. + +If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +it returns an empty array. **Visibility:** this method is **public**.
- **Returns**: this method returns **array<mixed,\ORM\Entity>** + **Returns**: this method returns **mixed|null**
-##### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$limit` | **integer** | Maximum number of entities to fetch | - #### ORM\Testing\EntityFetcherMock::andParenthesis @@ -4890,7 +4916,7 @@ Builds the statement from current where conditions, joins, columns and so on. #### ORM\Testing\EntityFetcherMock::getStatement ```php -private function getStatement(): \PDOStatement|boolean +protected function getStatement(): \PDOStatement|boolean ``` ##### Query database and return result @@ -4900,7 +4926,7 @@ Queries the database with current query and returns the resulted PDOStatement. If query failed it returns false. It also stores this failed result and to change the query afterwards will not change the result. -**Visibility:** this method is **private**. +**Visibility:** this method is **protected**.
**Returns**: this method returns **\PDOStatement|boolean**
@@ -5342,6 +5368,36 @@ can be set to true. +#### ORM\Testing\EntityFetcherMock::setFetchMode + +```php +public function setFetchMode( + integer $mode, null $classNameObject = null, array $ctorarfg = array() +): $this +``` + +##### Proxy to PDOStatement::setFetchMode() + +Please note that this will execute the query - further modifications will not have any effect. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$mode` | **integer** | | +| `$classNameObject` | **null** | | +| `$ctorarfg` | **array** | | + + + +**See Also:** + +* \PDOStatement::setFetchMode() #### ORM\Testing\EntityFetcherMock::setQuery ```php @@ -5615,6 +5671,7 @@ private function wherePrefix( string $bool ): string * [has](#ormentitymanagerhas) Check if the entity map has $entity * [map](#ormentitymanagermap) Map $entity in the entity map * [observe](#ormentitymanagerobserve) Observe $class using $observer +* [query](#ormentitymanagerquery) Get a query builder for $table * [setConnection](#ormentitymanagersetconnection) Add connection after instantiation * [setOption](#ormentitymanagersetoption) Set $option to $value * [setResolver](#ormentitymanagersetresolver) Overwrite the functionality of ::getInstance($class) by $resolver($class) @@ -6177,6 +6234,32 @@ For more information about model events please consult the [documentation](https +#### ORM\EntityManager::query + +```php +public function query( + string $table, string $alias = '' +): \ORM\QueryBuilder\QueryBuilder +``` + +##### Get a query builder for $table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | | +| `$alias` | **string** | | + + + #### ORM\EntityManager::setConnection ```php @@ -6361,6 +6444,7 @@ At the end you should call finish bulk insert otherwise you may loose data. * [has](#ormtestingentitymanagermockhas) Check if the entity map has $entity * [map](#ormtestingentitymanagermockmap) Map $entity in the entity map * [observe](#ormtestingentitymanagermockobserve) Observe $class using $observer +* [query](#ormtestingentitymanagermockquery) Get a query builder for $table * [retrieve](#ormtestingentitymanagermockretrieve) Retrieve an entity by $primaryKey * [setConnection](#ormtestingentitymanagermocksetconnection) Add connection after instantiation * [setOption](#ormtestingentitymanagermocksetoption) Set $option to $value @@ -6999,6 +7083,32 @@ For more information about model events please consult the [documentation](https +#### ORM\Testing\EntityManagerMock::query + +```php +public function query( + string $table, string $alias = '' +): \ORM\QueryBuilder\QueryBuilder +``` + +##### Get a query builder for $table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | | +| `$alias` | **string** | | + + + #### ORM\Testing\EntityManagerMock::retrieve ```php @@ -12109,6 +12219,7 @@ Supported: | **protected** | `$onClose` | **callable** | Callback to close the parenthesis | | **protected** | `$orderBy` | **array<string>** | Order by conditions get concatenated with comma | | **protected** | `$parent` | **ParenthesisInterface** | Parent parenthesis or query | +| **protected** | `$result` | ** \ PDOStatement** | The result object from PDO | | **protected** | `$tableName` | **string** | The table to query | | **protected** | `$where` | **array<string>** | Where conditions get concatenated with space | @@ -12117,6 +12228,7 @@ Supported: #### Methods * [__construct](#ormquerybuilderquerybuilder__construct) Constructor +* [all](#ormquerybuilderquerybuilderall) Get all rows from the query result * [andParenthesis](#ormquerybuilderquerybuilderandparenthesis) Add a parenthesis with AND * [andWhere](#ormquerybuilderquerybuilderandwhere) Add a where condition with AND. * [close](#ormquerybuilderquerybuilderclose) Close parenthesis @@ -12129,12 +12241,14 @@ Supported: * [getEntityManager](#ormquerybuilderquerybuildergetentitymanager) * [getExpression](#ormquerybuilderquerybuildergetexpression) Get the expression * [getQuery](#ormquerybuilderquerybuildergetquery) Get the query / select statement +* [getStatement](#ormquerybuilderquerybuildergetstatement) Query database and return result * [groupBy](#ormquerybuilderquerybuildergroupby) Group By $column * [join](#ormquerybuilderquerybuilderjoin) (Inner) join $tableName with $options * [leftJoin](#ormquerybuilderquerybuilderleftjoin) Left (outer) join $tableName with $options * [limit](#ormquerybuilderquerybuilderlimit) Set $limit * [modifier](#ormquerybuilderquerybuildermodifier) Add $modifier * [offset](#ormquerybuilderquerybuilderoffset) Set $offset +* [one](#ormquerybuilderquerybuilderone) Get the next row from the query result * [orderBy](#ormquerybuilderquerybuilderorderby) Order By $column in $direction * [orParenthesis](#ormquerybuilderquerybuilderorparenthesis) Add a parenthesis with OR * [orWhere](#ormquerybuilderquerybuilderorwhere) Add a where condition with OR. @@ -12142,6 +12256,7 @@ Supported: * [orWhereNotIn](#ormquerybuilderquerybuilderorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormquerybuilderquerybuilderparenthesis) Alias for andParenthesis * [rightJoin](#ormquerybuilderquerybuilderrightjoin) Right (outer) join $tableName with $options +* [setFetchMode](#ormquerybuilderquerybuildersetfetchmode) Proxy to PDOStatement::setFetchMode() * [where](#ormquerybuilderquerybuilderwhere) Alias for andWhere * [whereIn](#ormquerybuilderquerybuilderwherein) Add a where in condition with AND. * [whereNotIn](#ormquerybuilderquerybuilderwherenotin) Add a where not in condition with AND. @@ -12176,6 +12291,26 @@ It uses static::$defaultEntityManager if $entityManager is not given. +#### ORM\QueryBuilder\QueryBuilder::all + +```php +public function all(): mixed|null +``` + +##### Get all rows from the query result + +Please note that this will execute the query - further modifications will not have any effect. + +If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +it returns an empty array. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **mixed|null** +
+ + + #### ORM\QueryBuilder\QueryBuilder::andParenthesis ```php @@ -12453,6 +12588,26 @@ Builds the statement from current where conditions, joins, columns and so on. +#### ORM\QueryBuilder\QueryBuilder::getStatement + +```php +protected function getStatement(): \PDOStatement|boolean +``` + +##### Query database and return result + +Queries the database with current query and returns the resulted PDOStatement. + +If query failed it returns false. It also stores this failed result and to change the query afterwards will not +change the result. + +**Visibility:** this method is **protected**. +
+ **Returns**: this method returns **\PDOStatement|boolean** +
+ + + #### ORM\QueryBuilder\QueryBuilder::groupBy ```php @@ -12608,6 +12763,26 @@ Changes the offset (only with limit) where fetching starts in the query. +#### ORM\QueryBuilder\QueryBuilder::one + +```php +public function one(): mixed|null +``` + +##### Get the next row from the query result + +Please note that this will execute the query - further modifications will not have any effect. + +If the query fails you should get an exception. Anyway if we couldn't get a result or there are no more rows +it returns null. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **mixed|null** +
+ + + #### ORM\QueryBuilder\QueryBuilder::orderBy ```php @@ -12800,6 +12975,36 @@ can be set to true. +#### ORM\QueryBuilder\QueryBuilder::setFetchMode + +```php +public function setFetchMode( + integer $mode, null $classNameObject = null, array $ctorarfg = array() +): $this +``` + +##### Proxy to PDOStatement::setFetchMode() + +Please note that this will execute the query - further modifications will not have any effect. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$mode` | **integer** | | +| `$classNameObject` | **null** | | +| `$ctorarfg` | **array** | | + + + +**See Also:** + +* \PDOStatement::setFetchMode() #### ORM\QueryBuilder\QueryBuilder::where ```php @@ -14007,7 +14212,7 @@ Supported: * [__construct](#ormtestingentityfetchermockresult__construct) Constructor * [addEntities](#ormtestingentityfetchermockresultaddentities) Add entities to the result -* [all](#ormtestingentityfetchermockresultall) Fetch an array of entities +* [all](#ormtestingentityfetchermockresultall) Get all rows from the query result * [andParenthesis](#ormtestingentityfetchermockresultandparenthesis) Add a parenthesis with AND * [andWhere](#ormtestingentityfetchermockresultandwhere) Add a where condition with AND. * [close](#ormtestingentityfetchermockresultclose) Close parenthesis @@ -14035,7 +14240,7 @@ Supported: * [matches](#ormtestingentityfetchermockresultmatches) Add a regular expression that has to match * [modifier](#ormtestingentityfetchermockresultmodifier) Add $modifier * [offset](#ormtestingentityfetchermockresultoffset) Set $offset -* [one](#ormtestingentityfetchermockresultone) Fetch one entity +* [one](#ormtestingentityfetchermockresultone) Get the next row from the query result * [orderBy](#ormtestingentityfetchermockresultorderby) Order By $column in $direction * [orParenthesis](#ormtestingentityfetchermockresultorparenthesis) Add a parenthesis with OR * [orWhere](#ormtestingentityfetchermockresultorwhere) Add a where condition with OR. @@ -14043,6 +14248,7 @@ Supported: * [orWhereNotIn](#ormtestingentityfetchermockresultorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormtestingentityfetchermockresultparenthesis) Alias for andParenthesis * [rightJoin](#ormtestingentityfetchermockresultrightjoin) Right (outer) join $tableName with $options +* [setFetchMode](#ormtestingentityfetchermockresultsetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormtestingentityfetchermockresultsetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormtestingentityfetchermockresulttoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormtestingentityfetchermockresulttranslatecolumn) Translate attribute names in an expression to their column names @@ -14102,24 +14308,21 @@ public function addEntities( array<\ORM\Entity> $entities ): $this #### ORM\Testing\EntityFetcherMock\Result::all ```php -public function all( integer $limit ): array<\ORM\Entity> +public function all(): mixed|null ``` -##### Fetch an array of entities +##### Get all rows from the query result -When no $limit is set it fetches all entities in result set. +Please note that this will execute the query - further modifications will not have any effect. + +If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +it returns an empty array. **Visibility:** this method is **public**.
- **Returns**: this method returns **array<mixed,\ORM\Entity>** + **Returns**: this method returns **mixed|null**
-##### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$limit` | **integer** | Maximum number of entities to fetch | - #### ORM\Testing\EntityFetcherMock\Result::andParenthesis @@ -14486,7 +14689,7 @@ Builds the statement from current where conditions, joins, columns and so on. #### ORM\Testing\EntityFetcherMock\Result::getStatement ```php -private function getStatement(): \PDOStatement|boolean +protected function getStatement(): \PDOStatement|boolean ``` ##### Query database and return result @@ -14496,7 +14699,7 @@ Queries the database with current query and returns the resulted PDOStatement. If query failed it returns false. It also stores this failed result and to change the query afterwards will not change the result. -**Visibility:** this method is **private**. +**Visibility:** this method is **protected**.
**Returns**: this method returns **\PDOStatement|boolean**
@@ -14755,16 +14958,19 @@ Changes the offset (only with limit) where fetching starts in the query. #### ORM\Testing\EntityFetcherMock\Result::one ```php -public function one(): \ORM\Entity +public function one(): mixed|null ``` -##### Fetch one entity +##### Get the next row from the query result -If there is no more entity in the result set it returns null. +Please note that this will execute the query - further modifications will not have any effect. + +If the query fails you should get an exception. Anyway if we couldn't get a result or there are no more rows +it returns null. **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\Entity** + **Returns**: this method returns **mixed|null**
@@ -14961,6 +15167,36 @@ can be set to true. +#### ORM\Testing\EntityFetcherMock\Result::setFetchMode + +```php +public function setFetchMode( + integer $mode, null $classNameObject = null, array $ctorarfg = array() +): $this +``` + +##### Proxy to PDOStatement::setFetchMode() + +Please note that this will execute the query - further modifications will not have any effect. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$mode` | **integer** | | +| `$classNameObject` | **null** | | +| `$ctorarfg` | **array** | | + + + +**See Also:** + +* \PDOStatement::setFetchMode() #### ORM\Testing\EntityFetcherMock\Result::setQuery ```php diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index 9adbf49..2852c02 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -321,14 +321,14 @@ protected function syncInserted(Entity ...$entities) $primary = array_combine($vars, $cols); $cols = array_map([$this, 'escapeIdentifier'], $cols); - $query = new QueryBuilder($this->escapeIdentifier($entity::getTableName()), '', $this->entityManager); - $query->whereIn($cols, array_map(function (Entity $entity) { - return $entity->getPrimaryKey(); - }, $entities)); + $query = $this->entityManager->query($this->escapeIdentifier($entity::getTableName())) + ->whereIn($cols, array_map(function (Entity $entity) { + return $entity->getPrimaryKey(); + }, $entities)) + ->setFetchMode(PDO::FETCH_ASSOC); - $statement = $this->entityManager->getConnection()->query($query->getQuery()); $left = $entities; - while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { + while ($row = $query->one()) { foreach ($left as $k => $entity) { foreach ($primary as $var => $col) { if ($entity->$var != $row[$col]) { diff --git a/src/EntityFetcher.php b/src/EntityFetcher.php index 2f4ba3e..858d7cc 100644 --- a/src/EntityFetcher.php +++ b/src/EntityFetcher.php @@ -37,10 +37,6 @@ class EntityFetcher extends QueryBuilder * @var string|Entity */ protected $class; - /** The result object from PDO - * @var PDOStatement */ - protected $result; - /** The query to execute (overwrites other settings) * @var string|QueryBuilderInterface */ protected $query; @@ -62,22 +58,6 @@ public function __construct(EntityManager $entityManager, $class) $this->modifier = [ 'DISTINCT' ]; } - /** @return static - * @internal - */ - public function columns(array $columns = null) - { - return $this; - } - - /** @return static - * @internal - */ - public function column($column, $args = [], $alias = '') - { - return $this; - } - /** * Replaces questionmarks in $expression with $args * @@ -118,13 +98,8 @@ public function buildWhereInExpression($column, array $values, $inverse = false) */ public function one() { - $result = $this->getStatement(); - if (!$result) { - return null; - } - - $data = $result->fetch(PDO::FETCH_ASSOC); - + parent::setFetchMode(PDO::FETCH_ASSOC); + $data = parent::one(); if (!$data) { return null; } @@ -183,24 +158,6 @@ public function count() return (int) $this->entityManager->getConnection()->query($query)->fetchColumn(); } - /** - * Query database and return result - * - * Queries the database with current query and returns the resulted PDOStatement. - * - * If query failed it returns false. It also stores this failed result and to change the query afterwards will not - * change the result. - * - * @return PDOStatement|bool - */ - private function getStatement() - { - if ($this->result === null) { - $this->result = $this->entityManager->getConnection()->query($this->getQuery()); - } - return $this->result; - } - /** {@inheritdoc} */ public function getQuery() { @@ -228,4 +185,25 @@ public function setQuery($query, $args = null) $this->query = $query; return $this; } + + /** @return static + * @internal */ + public function columns(array $columns = null) + { + return $this; + } + + /** @return static + * @internal */ + public function column($column, $args = [], $alias = '') + { + return $this; + } + + /** @return static + * @internal */ + public function setFetchMode($mode, $classNameObject = null, array $ctorarfg = []) + { + return $this; + } } diff --git a/src/EntityManager.php b/src/EntityManager.php index 42c96bc..b7de49b 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -15,6 +15,7 @@ use ORM\Exception\NoEntity; use ORM\Observer\AbstractObserver; use ORM\Observer\CallbackObserver; +use ORM\QueryBuilder\QueryBuilder; use PDO; use ReflectionClass; @@ -363,6 +364,18 @@ public function getNamer() return $this->namer; } + /** + * Get a query builder for $table + * + * @param string $table + * @param string $alias + * @return QueryBuilder + */ + public function query($table, $alias = '') + { + return new QueryBuilder($table, $alias, $this); + } + /** * Synchronizing $entity with database * diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index b3933ea..3f281f9 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -3,6 +3,7 @@ namespace ORM\QueryBuilder; use ORM\EntityManager; +use PDOStatement; /** * Build a ansi sql query / select statement @@ -66,6 +67,10 @@ class QueryBuilder extends Parenthesis implements QueryBuilderInterface * @var EntityManager */ public static $defaultEntityManager; + /** The result object from PDO + * @var PDOStatement */ + protected $result; + /** @noinspection PhpMissingParentConstructorInspection */ /** * Constructor @@ -272,6 +277,68 @@ public function offset($offset) return $this; } + /** + * Proxy to PDOStatement::setFetchMode() + * + * Please note that this will execute the query - further modifications will not have any effect. + * + * @param int $mode + * @param null $classNameObject + * @param array $ctorarfg + * @return $this + * @see PDOStatement::setFetchMode() + */ + public function setFetchMode($mode, $classNameObject = null, array $ctorarfg = []) + { + $result = $this->getStatement(); + if (!$result) { + return $this; + } + + $result->setFetchMode($mode, $classNameObject, $ctorarfg); + return $this; + } + + /** + * Get the next row from the query result + * + * Please note that this will execute the query - further modifications will not have any effect. + * + * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no more rows + * it returns null. + * + * @return mixed|null + */ + public function one() + { + $result = $this->getStatement(); + if (!$result) { + return null; + } + + return $result->fetch() ?: null; + } + + /** + * Get all rows from the query result + * + * Please note that this will execute the query - further modifications will not have any effect. + * + * If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows + * it returns an empty array. + * + * @return mixed|null + */ + public function all() + { + $result = $this->getStatement(); + if (!$result) { + return []; + } + + return $result->fetchAll(); + } + /** {@inheritdoc} */ public function getQuery() { @@ -286,6 +353,24 @@ public function getQuery() . ($this->limit ? ' LIMIT ' . $this->limit . ($this->offset ? ' OFFSET ' . $this->offset : '') : ''); } + /** + * Query database and return result + * + * Queries the database with current query and returns the resulted PDOStatement. + * + * If query failed it returns false. It also stores this failed result and to change the query afterwards will not + * change the result. + * + * @return PDOStatement|bool + */ + protected function getStatement() + { + if ($this->result === null) { + $this->result = $this->entityManager->getConnection()->query($this->getQuery()) ?: false; + } + return $this->result; + } + /** {@inheritdoc} */ public function modifier($modifier) { diff --git a/tests/Dbal/BulkInsertTest.php b/tests/Dbal/BulkInsertTest.php index e7e8e6e..93b1c93 100644 --- a/tests/Dbal/BulkInsertTest.php +++ b/tests/Dbal/BulkInsertTest.php @@ -81,7 +81,8 @@ public function bulkInsertWithCompositePrimaryKey() $this->pdo->shouldReceive('query')->with(m::pattern( '/SELECT \* FROM .* WHERE \("id","name"\) IN \((VALUES )?(\(.*\))(,\(.*\))*\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC) + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->with() ->times(3)->andReturn( ['id' => 23, 'name' => 'business', 'number' => '+1 555 2424', 'created' => date('c')], ['id' => 23, 'name' => 'mobile', 'number' => '+1 555 2323', 'created' => date('c')], diff --git a/tests/Dbal/Sqlite/InsertTest.php b/tests/Dbal/Sqlite/InsertTest.php index ac97bcc..17f7225 100644 --- a/tests/Dbal/Sqlite/InsertTest.php +++ b/tests/Dbal/Sqlite/InsertTest.php @@ -30,7 +30,8 @@ public function buildsValidQueryForCompositeKeys() $this->pdo->shouldReceive('query')->with(m::pattern( '/SELECT \* FROM .* WHERE \("id","name"\) IN \((\(.*\))(,\(.*\))*\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC) + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->with() ->times(2)->andReturn( ['id' => 1, 'name' => 'mobile', 'number' => '+1 555 123', 'created' => date('c')], false diff --git a/tests/EntityManager/DataModificationTest.php b/tests/EntityManager/DataModificationTest.php index f42e503..8ccdd10 100644 --- a/tests/EntityManager/DataModificationTest.php +++ b/tests/EntityManager/DataModificationTest.php @@ -218,7 +218,8 @@ public function insertReturnsTrue() ->once()->andReturn(m::mock(\PDOStatement::class)); $this->pdo->shouldReceive('query')->with('SELECT * FROM "psr0_studly_caps" WHERE "id" IN (42)') ->once()->andReturn($statement = m::mock(\PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC) + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->with() ->twice()->andReturn(['id' => 42, 'foo' => 'bar'], false); $result = $this->em->insert($entity); @@ -249,7 +250,8 @@ public function doesNotUseAutoIncrement($driver) ->once()->andReturn(m::mock(\PDOStatement::class)); $this->pdo->shouldReceive('query')->with(m::pattern('/^SELECT \* FROM .* WHERE .* IN (.*)/')) ->once()->andReturn($statement = m::mock(\PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC) + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->with() ->twice()->andReturn(['id' => 42, 'foo' => 'bar'], false); $result = $this->em->insert($entity, false); @@ -318,7 +320,8 @@ public function insertReturnsAutoIncrementPgsql() $this->pdo->shouldReceive('query')->with(m::pattern( '/SELECT \* FROM .* WHERE "id" IN \(42\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class))->ordered(); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC) + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->with() ->twice()->andReturn(['id' => 42, 'foo' => 'bar'], false)->ordered(); $result = $this->em->insert($entity); diff --git a/tests/EntityManager/EntityFetcherTest.php b/tests/EntityManager/EntityFetcherTest.php index d2eddd8..5232382 100644 --- a/tests/EntityManager/EntityFetcherTest.php +++ b/tests/EntityManager/EntityFetcherTest.php @@ -44,6 +44,7 @@ public function returnsNullWhenResultIsEmpty() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); + $statement->shouldReceive('setFetchMode')->andReturn(true); $statement->shouldReceive('fetch')->andReturn(false); $result = $fetcher->one(); @@ -121,7 +122,8 @@ public function returnsAnEntity() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); - $statement->shouldReceive('fetch')->once()->with(\PDO::FETCH_ASSOC)->andReturn([ + $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); + $statement->shouldReceive('fetch')->once()->with()->andReturn([ 'id' => 42, 'name' => 'mobile', 'number' => '+49 151 00000000' @@ -144,6 +146,7 @@ public function returnsPreviouslyMapped() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); + $statement->shouldReceive('setFetchMode')->andReturnTrue(); $statement->shouldReceive('fetch')->andReturn([ 'id' => 42, 'name' => 'mobile', @@ -169,6 +172,7 @@ public function updatesOriginalData() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); + $statement->shouldReceive('setFetchMode')->andReturnTrue(); $statement->shouldReceive('fetch')->andReturn([ 'id' => 42, 'name' => 'mobile', @@ -192,6 +196,7 @@ public function resetsData() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); + $statement->shouldReceive('setFetchMode')->andReturnTrue(); $statement->shouldReceive('fetch')->andReturn([ 'id' => 42, 'name' => 'mobile', @@ -217,6 +222,7 @@ public function resetsOnlyNonDirty() $fetcher = $this->em->fetch(ContactPhone::class); $statement = \Mockery::mock(\PDOStatement::class); $this->pdo->shouldReceive('query')->andReturn($statement); + $statement->shouldReceive('setFetchMode')->andReturnTrue(); $statement->shouldReceive('fetch')->andReturn([ 'id' => 42, 'name' => 'mobile', diff --git a/tests/EntityManager/MappingTest.php b/tests/EntityManager/MappingTest.php index b9281e6..1480093 100644 --- a/tests/EntityManager/MappingTest.php +++ b/tests/EntityManager/MappingTest.php @@ -148,7 +148,8 @@ public function fetchGetsEntityFromPrimaryKey() $this->pdo->shouldReceive('query')->once() ->with('SELECT DISTINCT t0.* FROM "studly_caps" AS t0 WHERE "t0"."id" = 42') ->andReturn($statement); - $statement->shouldReceive('fetch')->once()->with(\PDO::FETCH_ASSOC)->andReturn( + $statement->shouldReceive('setFetchMode')->andReturnTrue(); + $statement->shouldReceive('fetch')->once()->with()->andReturn( ['id' => 42, 'col1' => 'hallo', 'col2' => 'welt'] ); diff --git a/tests/Relation/ManyToManyTest.php b/tests/Relation/ManyToManyTest.php index 06adf6f..904a3a1 100644 --- a/tests/Relation/ManyToManyTest.php +++ b/tests/Relation/ManyToManyTest.php @@ -84,7 +84,8 @@ public function returnsPreviouslyMappedWithGetAll() ' JOIN "article_category" ON "article_category"."category_id" = "t0"."id"' . ' WHERE "article_category"."article_id" = 42' )->once()->andReturn($statement = m::mock(PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC)->times(3) + $statement->shouldReceive('setFetchMode')->andReturnTrue(); + $statement->shouldReceive('fetch')->with()->times(3) ->andReturn( ['id' => 12, 'name' => 'Foos'], ['id' => 33, 'name' => 'Bars'], @@ -106,7 +107,8 @@ public function fetchesAllEntitiesWithOneQuery() ' JOIN "article_category" ON "article_category"."category_id" = "t0"."id"' . ' WHERE "article_category"."article_id" = 42' )->once()->andReturn($statement = m::mock(PDOStatement::class)); - $statement->shouldReceive('fetch')->with(\PDO::FETCH_ASSOC)->times(3) + $statement->shouldReceive('setFetchMode')->andReturnTrue(); + $statement->shouldReceive('fetch')->with()->times(3) ->andReturn( ['id' => 1, 'name' => 'Foos'], ['id' => 2, 'name' => 'Bars'], From b959a1e84cc7297949194e18afde9c6fcaeee6cd Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Sat, 31 Oct 2020 10:08:59 +0100 Subject: [PATCH 02/16] implement internal cursor to be able to reset the position of resultset --- docs/reference.md | 154 ++++++++++++++++----- src/QueryBuilder/QueryBuilder.php | 46 +++++- src/QueryBuilder/QueryBuilderInterface.php | 2 +- tests/EntityManager/EntityFetcherTest.php | 32 +++++ tests/QueryBuilder/FetchTest.php | 41 ++++++ 5 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 tests/QueryBuilder/FetchTest.php diff --git a/docs/reference.md b/docs/reference.md index a28dd09..a30e464 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3338,6 +3338,7 @@ Supported: | **protected** | `$class` | **string | Entity** | The entity class that we want to fetch | | **protected** | `$classMapping` | **array<string[]>** | The class to alias mapping and vise versa | | **protected** | `$columns` | **array | null** | Columns to fetch (null is equal to ['*']) | +| **protected** | `$cursor` | **integer** | The position of the cursor | | **public static** | `$defaultEntityManager` | **EntityManager** | The default EntityManager to use to for quoting | | **protected** | `$entityManager` | **EntityManager** | EntityManager to use for quoting | | **protected** | `$groupBy` | **array<string>** | Group by conditions get concatenated with comma | @@ -3350,6 +3351,7 @@ Supported: | **protected** | `$parent` | **QueryBuilder \ ParenthesisInterface** | Parent parenthesis or query | | **protected** | `$query` | **string | QueryBuilder \ QueryBuilderInterface** | The query to execute (overwrites other settings) | | **protected** | `$result` | ** \ PDOStatement** | The result object from PDO | +| **protected** | `$rows` | **array** | The rows returned | | **protected** | `$tableName` | **string** | The table to query | | **protected** | `$where` | **array<string>** | Where conditions get concatenated with space | @@ -3369,7 +3371,7 @@ Supported: * [createRelatedJoin](#ormentityfetchercreaterelatedjoin) Create the join with $join type * [first](#ormentityfetcherfirst) Get the first item of an array * [fullJoin](#ormentityfetcherfulljoin) Full (outer) join $tableName with $options -* [getDefaultOperator](#ormentityfetchergetdefaultoperator) +* [getDefaultOperator](#ormentityfetchergetdefaultoperator) Get the default operator for $value * [getEntityManager](#ormentityfetchergetentitymanager) * [getExpression](#ormentityfetchergetexpression) Get the expression * [getQuery](#ormentityfetchergetquery) Get the query / select statement @@ -3390,6 +3392,7 @@ Supported: * [orWhereIn](#ormentityfetcherorwherein) Add a where in condition with OR. * [orWhereNotIn](#ormentityfetcherorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormentityfetcherparenthesis) Alias for andParenthesis +* [reset](#ormentityfetcherreset) Reset the position of the cursor to the first row * [rightJoin](#ormentityfetcherrightjoin) Right (outer) join $tableName with $options * [setFetchMode](#ormentityfetchersetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormentityfetchersetquery) Set a raw query or use different QueryBuilder @@ -3528,7 +3531,7 @@ public function close(): ORM\QueryBuilder\QueryBuilderInterface|ORM\QueryBuilder ```php public function column( string $column, array $args = array(), string $alias = '' -): ORM\QueryBuilder\QueryBuilder +): $this ``` ##### Add $column @@ -3537,7 +3540,7 @@ Optionally you can provide an expression with question marks as placeholders fil **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** + **Returns**: this method returns **$this**
##### Parameters @@ -3700,21 +3703,23 @@ ATTENTION: here the default value of empty got changed - defaults to yes #### ORM\EntityFetcher::getDefaultOperator ```php -private function getDefaultOperator( $value ) +private function getDefaultOperator( $value ): string ``` +##### Get the default operator for $value - +Arrays use `IN` by default - all others use `=` **Visibility:** this method is **private**.
- + **Returns**: this method returns **string** +
##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$value` | | | +| `$value` | **mixed** | The value to determine the operator | @@ -4192,6 +4197,23 @@ public function parenthesis(): $this +#### ORM\EntityFetcher::reset + +```php +public function reset(): $this +``` + +##### Reset the position of the cursor to the first row + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ + + #### ORM\EntityFetcher::rightJoin ```php @@ -4483,6 +4505,7 @@ Supported: | **protected** | `$classMapping` | **array<string[]>** | The class to alias mapping and vise versa | | **protected** | `$columns` | **array | null** | Columns to fetch (null is equal to ['*']) | | **protected** | `$currentResult` | **array** | | +| **protected** | `$cursor` | **integer** | The position of the cursor | | **public static** | `$defaultEntityManager` | ** \ ORM \ EntityManager** | The default EntityManager to use to for quoting | | **public** | `$entityManager` | **EntityManagerMock** | | | **protected** | `$groupBy` | **array<string>** | Group by conditions get concatenated with comma | @@ -4495,6 +4518,7 @@ Supported: | **protected** | `$parent` | ** \ ORM \ QueryBuilder \ ParenthesisInterface** | Parent parenthesis or query | | **protected** | `$query` | **string | \ ORM \ QueryBuilder \ QueryBuilderInterface** | The query to execute (overwrites other settings) | | **protected** | `$result` | ** \ PDOStatement** | The result object from PDO | +| **protected** | `$rows` | **array** | The rows returned | | **protected** | `$tableName` | **string** | The table to query | | **protected** | `$where` | **array<string>** | Where conditions get concatenated with space | @@ -4514,7 +4538,7 @@ Supported: * [createRelatedJoin](#ormtestingentityfetchermockcreaterelatedjoin) Create the join with $join type * [first](#ormtestingentityfetchermockfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockfulljoin) Full (outer) join $tableName with $options -* [getDefaultOperator](#ormtestingentityfetchermockgetdefaultoperator) +* [getDefaultOperator](#ormtestingentityfetchermockgetdefaultoperator) Get the default operator for $value * [getEntityManager](#ormtestingentityfetchermockgetentitymanager) * [getExpression](#ormtestingentityfetchermockgetexpression) Get the expression * [getQuery](#ormtestingentityfetchermockgetquery) Get the query / select statement @@ -4535,6 +4559,7 @@ Supported: * [orWhereIn](#ormtestingentityfetchermockorwherein) Add a where in condition with OR. * [orWhereNotIn](#ormtestingentityfetchermockorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormtestingentityfetchermockparenthesis) Alias for andParenthesis +* [reset](#ormtestingentityfetchermockreset) Reset the position of the cursor to the first row * [rightJoin](#ormtestingentityfetchermockrightjoin) Right (outer) join $tableName with $options * [setFetchMode](#ormtestingentityfetchermocksetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormtestingentityfetchermocksetquery) Set a raw query or use different QueryBuilder @@ -4580,7 +4605,7 @@ public function all(): mixed|null Please note that this will execute the query - further modifications will not have any effect. -If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows it returns an empty array. **Visibility:** this method is **public**. @@ -4670,7 +4695,7 @@ public function close(): ORM\QueryBuilder\QueryBuilderInterface|ORM\QueryBuilder ```php public function column( string $column, array $args = array(), string $alias = '' -): ORM\QueryBuilder\QueryBuilder +): $this ``` ##### Add $column @@ -4679,7 +4704,7 @@ Optionally you can provide an expression with question marks as placeholders fil **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** + **Returns**: this method returns **$this**
##### Parameters @@ -4838,21 +4863,23 @@ ATTENTION: here the default value of empty got changed - defaults to yes #### ORM\Testing\EntityFetcherMock::getDefaultOperator ```php -private function getDefaultOperator( $value ) +private function getDefaultOperator( $value ): string ``` +##### Get the default operator for $value - +Arrays use `IN` by default - all others use `=` **Visibility:** this method is **private**.
- + **Returns**: this method returns **string** +
##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$value` | | | +| `$value` | **mixed** | The value to determine the operator | @@ -5330,6 +5357,23 @@ public function parenthesis(): $this +#### ORM\Testing\EntityFetcherMock::reset + +```php +public function reset(): $this +``` + +##### Reset the position of the cursor to the first row + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ + + #### ORM\Testing\EntityFetcherMock::rightJoin ```php @@ -12186,6 +12230,7 @@ Supported: |------------|------|------|---------------------------------------| | **protected** | `$alias` | **string** | The alias of the main table | | **protected** | `$columns` | **array | null** | Columns to fetch (null is equal to ['*']) | +| **protected** | `$cursor` | **integer** | The position of the cursor | | **public static** | `$defaultEntityManager` | ** \ ORM \ EntityManager** | The default EntityManager to use to for quoting | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | EntityManager to use for quoting | | **protected** | `$groupBy` | **array<string>** | Group by conditions get concatenated with comma | @@ -12197,6 +12242,7 @@ Supported: | **protected** | `$orderBy` | **array<string>** | Order by conditions get concatenated with comma | | **protected** | `$parent` | **ParenthesisInterface** | Parent parenthesis or query | | **protected** | `$result` | ** \ PDOStatement** | The result object from PDO | +| **protected** | `$rows` | **array** | The rows returned | | **protected** | `$tableName` | **string** | The table to query | | **protected** | `$where` | **array<string>** | Where conditions get concatenated with space | @@ -12214,7 +12260,7 @@ Supported: * [convertPlaceholders](#ormquerybuilderquerybuilderconvertplaceholders) Replaces question marks in $expression with $args * [first](#ormquerybuilderquerybuilderfirst) Get the first item of an array * [fullJoin](#ormquerybuilderquerybuilderfulljoin) Full (outer) join $tableName with $options -* [getDefaultOperator](#ormquerybuilderquerybuildergetdefaultoperator) +* [getDefaultOperator](#ormquerybuilderquerybuildergetdefaultoperator) Get the default operator for $value * [getEntityManager](#ormquerybuilderquerybuildergetentitymanager) * [getExpression](#ormquerybuilderquerybuildergetexpression) Get the expression * [getQuery](#ormquerybuilderquerybuildergetquery) Get the query / select statement @@ -12232,6 +12278,7 @@ Supported: * [orWhereIn](#ormquerybuilderquerybuilderorwherein) Add a where in condition with OR. * [orWhereNotIn](#ormquerybuilderquerybuilderorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormquerybuilderquerybuilderparenthesis) Alias for andParenthesis +* [reset](#ormquerybuilderquerybuilderreset) Reset the position of the cursor to the first row * [rightJoin](#ormquerybuilderquerybuilderrightjoin) Right (outer) join $tableName with $options * [setFetchMode](#ormquerybuilderquerybuildersetfetchmode) Proxy to PDOStatement::setFetchMode() * [where](#ormquerybuilderquerybuilderwhere) Alias for andWhere @@ -12278,7 +12325,7 @@ public function all(): mixed|null Please note that this will execute the query - further modifications will not have any effect. -If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows it returns an empty array. **Visibility:** this method is **public**. @@ -12368,7 +12415,7 @@ public function close(): ORM\QueryBuilder\QueryBuilderInterface|ORM\QueryBuilder ```php public function column( string $column, array $args = array(), string $alias = '' -): ORM\QueryBuilder\QueryBuilder +): $this ``` ##### Add $column @@ -12377,7 +12424,7 @@ Optionally you can provide an expression with question marks as placeholders fil **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** + **Returns**: this method returns **$this**
##### Parameters @@ -12495,21 +12542,23 @@ ATTENTION: here the default value of empty got changed - defaults to yes #### ORM\QueryBuilder\QueryBuilder::getDefaultOperator ```php -private function getDefaultOperator( $value ) +private function getDefaultOperator( $value ): string ``` +##### Get the default operator for $value - +Arrays use `IN` by default - all others use `=` **Visibility:** this method is **private**.
- + **Returns**: this method returns **string** +
##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$value` | | | +| `$value` | **mixed** | The value to determine the operator | @@ -12919,6 +12968,23 @@ public function parenthesis(): $this +#### ORM\QueryBuilder\QueryBuilder::reset + +```php +public function reset(): $this +``` + +##### Reset the position of the cursor to the first row + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ + + #### ORM\QueryBuilder\QueryBuilder::rightJoin ```php @@ -13235,7 +13301,7 @@ public function close(): ORM\QueryBuilder\QueryBuilderInterface|ORM\QueryBuilder ```php public function column( string $column, array $args = array(), string $alias = '' -): ORM\QueryBuilder\QueryBuilder +): $this ``` ##### Add $column @@ -13244,7 +13310,7 @@ Optionally you can provide an expression with question marks as placeholders fil **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** + **Returns**: this method returns **$this**
##### Parameters @@ -14164,6 +14230,7 @@ Supported: | **protected** | `$class` | **string | \ ORM \ Entity** | The entity class that we want to fetch | | **protected** | `$classMapping` | **array<string[]>** | The class to alias mapping and vise versa | | **protected** | `$columns` | **array | null** | Columns to fetch (null is equal to ['*']) | +| **protected** | `$cursor` | **integer** | The position of the cursor | | **public static** | `$defaultEntityManager` | ** \ ORM \ EntityManager** | The default EntityManager to use to for quoting | | **protected** | `$entities` | **array< \ ORM \ Entity>** | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | EntityManager to use for quoting | @@ -14178,6 +14245,7 @@ Supported: | **protected** | `$query` | **string | \ ORM \ QueryBuilder \ QueryBuilderInterface** | The query to execute (overwrites other settings) | | **protected** | `$regularExpressions` | **array<string>** | | | **protected** | `$result` | ** \ PDOStatement** | The result object from PDO | +| **protected** | `$rows` | **array** | The rows returned | | **protected** | `$tableName` | **string** | The table to query | | **protected** | `$where` | **array<string>** | Where conditions get concatenated with space | @@ -14199,7 +14267,7 @@ Supported: * [createRelatedJoin](#ormtestingentityfetchermockresultcreaterelatedjoin) Create the join with $join type * [first](#ormtestingentityfetchermockresultfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockresultfulljoin) Full (outer) join $tableName with $options -* [getDefaultOperator](#ormtestingentityfetchermockresultgetdefaultoperator) +* [getDefaultOperator](#ormtestingentityfetchermockresultgetdefaultoperator) Get the default operator for $value * [getEntities](#ormtestingentityfetchermockresultgetentities) Get the entities for this result * [getEntityManager](#ormtestingentityfetchermockresultgetentitymanager) * [getExpression](#ormtestingentityfetchermockresultgetexpression) Get the expression @@ -14222,6 +14290,7 @@ Supported: * [orWhereIn](#ormtestingentityfetchermockresultorwherein) Add a where in condition with OR. * [orWhereNotIn](#ormtestingentityfetchermockresultorwherenotin) Add a where not in condition with OR. * [parenthesis](#ormtestingentityfetchermockresultparenthesis) Alias for andParenthesis +* [reset](#ormtestingentityfetchermockresultreset) Reset the position of the cursor to the first row * [rightJoin](#ormtestingentityfetchermockresultrightjoin) Right (outer) join $tableName with $options * [setFetchMode](#ormtestingentityfetchermockresultsetfetchmode) Proxy to PDOStatement::setFetchMode() * [setQuery](#ormtestingentityfetchermockresultsetquery) Set a raw query or use different QueryBuilder @@ -14290,7 +14359,7 @@ public function all(): mixed|null Please note that this will execute the query - further modifications will not have any effect. -If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows +If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows it returns an empty array. **Visibility:** this method is **public**. @@ -14380,7 +14449,7 @@ public function close(): ORM\QueryBuilder\QueryBuilderInterface|ORM\QueryBuilder ```php public function column( string $column, array $args = array(), string $alias = '' -): ORM\QueryBuilder\QueryBuilder +): $this ``` ##### Add $column @@ -14389,7 +14458,7 @@ Optionally you can provide an expression with question marks as placeholders fil **Visibility:** this method is **public**.
- **Returns**: this method returns **\ORM\QueryBuilder\QueryBuilder** + **Returns**: this method returns **$this**
##### Parameters @@ -14574,21 +14643,23 @@ ATTENTION: here the default value of empty got changed - defaults to yes #### ORM\Testing\EntityFetcherMock\Result::getDefaultOperator ```php -private function getDefaultOperator( $value ) +private function getDefaultOperator( $value ): string ``` +##### Get the default operator for $value - +Arrays use `IN` by default - all others use `=` **Visibility:** this method is **private**.
- + **Returns**: this method returns **string** +
##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$value` | | | +| `$value` | **mixed** | The value to determine the operator | @@ -15109,6 +15180,23 @@ public function parenthesis(): $this +#### ORM\Testing\EntityFetcherMock\Result::reset + +```php +public function reset(): $this +``` + +##### Reset the position of the cursor to the first row + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **$this** +
+ + + #### ORM\Testing\EntityFetcherMock\Result::rightJoin ```php diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index 74656bf..46b70f8 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -71,6 +71,14 @@ class QueryBuilder extends Parenthesis implements QueryBuilderInterface * @var PDOStatement */ protected $result; + /** The rows returned + * @var array */ + protected $rows = []; + + /** The position of the cursor + * @var int */ + protected $cursor = 0; + /** @noinspection PhpMissingParentConstructorInspection */ /** * Constructor @@ -203,6 +211,14 @@ private function first($array) return null; } + /** + * Get the default operator for $value + * + * Arrays use `IN` by default - all others use `=` + * + * @param mixed $value The value to determine the operator + * @return string + */ private function getDefaultOperator($value) { if (is_array($value)) { @@ -316,7 +332,13 @@ public function one() return null; } - return $result->fetch() ?: null; + $cursor = $this->cursor; + if (!isset($this->rows[$cursor])) { + $this->rows[$cursor] = $result->fetch(); + } + + $this->cursor++; + return $this->rows[$cursor] ?: null; } /** @@ -324,19 +346,31 @@ public function one() * * Please note that this will execute the query - further modifications will not have any effect. * - * If the query fails you should get an exception. Anyway if we couldn't get a result or there no rows + * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows * it returns an empty array. * * @return mixed|null */ public function all() { - $result = $this->getStatement(); - if (!$result) { - return []; + $result = []; + while ($next = $this->one()) { + $result[] = $next; } - return $result->fetchAll(); + return $result; + } + + /** + * Reset the position of the cursor to the first row + * + * @return $this + */ + public function reset() + { + $this->cursor = 0; + + return $this; } /** {@inheritdoc} */ diff --git a/src/QueryBuilder/QueryBuilderInterface.php b/src/QueryBuilder/QueryBuilderInterface.php index e40245e..2bee9bd 100644 --- a/src/QueryBuilder/QueryBuilderInterface.php +++ b/src/QueryBuilder/QueryBuilderInterface.php @@ -29,7 +29,7 @@ public function columns(array $columns = null); * @param string $column Column or expression to fetch * @param array $args Arguments for expression * @param string $alias Alias for the column - * @return QueryBuilder + * @return $this */ public function column($column, $args = [], $alias = ''); diff --git a/tests/EntityManager/EntityFetcherTest.php b/tests/EntityManager/EntityFetcherTest.php index 5232382..1fcfdde 100644 --- a/tests/EntityManager/EntityFetcherTest.php +++ b/tests/EntityManager/EntityFetcherTest.php @@ -310,6 +310,38 @@ public function allReturnsRemainingEntities() ], $contactPhones); } + /** @test */ + public function returnsAllItemsAfterReset() + { + $e1 = $this->em->map(new ContactPhone([ + 'id' => 42, + 'name' => 'mobile' + ], $this->em, true)); + $e2 = $this->em->map(new ContactPhone([ + 'id' => 43, + 'name' => 'mobile' + ], $this->em, true)); + $e3 = $this->em->map(new ContactPhone([ + 'id' => 44, + 'name' => 'mobile' + ], $this->em, true)); + + /** @var EntityFetcher|Mock $fetcher */ + $fetcher = $this->em->fetch(ContactPhone::class); + $this->pdo->shouldReceive('query')->once() + ->with('SELECT DISTINCT t0.* FROM "contact_phone" AS t0') + ->andReturn($statement = \Mockery::mock(\PDOStatement::class)); + $statement->shouldReceive('setFetchMode')->andReturnTrue(); + $statement->shouldReceive('fetch')->with()->times(4) + ->andReturn($e1->getData(), $e2->getData(), $e3->getData(), false); + + $fetcher->all(); + + $contactPhones = $fetcher->reset()->all(); + + self::assertSame([$e1, $e2, $e3], $contactPhones); + } + /** @test */ public function allReturnsLimitedAmount() { diff --git a/tests/QueryBuilder/FetchTest.php b/tests/QueryBuilder/FetchTest.php new file mode 100644 index 0000000..188b2ad --- /dev/null +++ b/tests/QueryBuilder/FetchTest.php @@ -0,0 +1,41 @@ +em->query('table'); + $row = ['id' => 42, 'name' => 'foobar']; + + $this->pdo->shouldReceive('query')->with('SELECT * FROM table') + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('fetch')->with()->once()->andReturn($row); + + $result = $query->one(); + + self::assertSame($row, $result); + } + + /** @test */ + public function returnsAllRows() + { + $query = $this->em->query('table'); + $row1 = ['id' => 42, 'name' => 'foobar']; + $row2 = ['id' => 23, 'name' => 'foo bar']; + + $this->pdo->shouldReceive('query')->with('SELECT * FROM table') + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('fetch')->with()->times(3) + ->andReturn($row1, $row2, false); + + $result = $query->all(); + + self::assertSame([$row1, $row2], $result); + } +} From 2dd411abe61a651153b3222606862b3124b7bae5 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Mon, 2 Nov 2020 08:49:48 +0100 Subject: [PATCH 03/16] extract query execution methods to trait --- src/QueryBuilder/ExecutesQueries.php | 118 ++++++++++++++++++++++ src/QueryBuilder/QueryBuilder.php | 111 +------------------- tests/EntityManager/EntityFetcherTest.php | 10 ++ 3 files changed, 129 insertions(+), 110 deletions(-) create mode 100644 src/QueryBuilder/ExecutesQueries.php diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php new file mode 100644 index 0000000..daef66d --- /dev/null +++ b/src/QueryBuilder/ExecutesQueries.php @@ -0,0 +1,118 @@ +getStatement(); + if (!$result) { + return $this; + } + + $result->setFetchMode($mode, $classNameObject, $ctorarfg); + return $this; + } + + /** + * Get the next row from the query result + * + * Please note that this will execute the query - further modifications will not have any effect. + * + * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no more rows + * it returns null. + * + * @return mixed|null + */ + public function one() + { + $result = $this->getStatement(); + if (!$result) { + return null; + } + + $cursor = $this->cursor; + if (!isset($this->rows[$cursor])) { + $this->rows[$cursor] = $result->fetch(); + } + + $this->cursor++; + return $this->rows[$cursor] ?: null; + } + + /** + * Get all rows from the query result + * + * Please note that this will execute the query - further modifications will not have any effect. + * + * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows + * it returns an empty array. + * + * @return mixed|null + */ + public function all() + { + $result = []; + while ($next = $this->one()) { + $result[] = $next; + } + + return $result; + } + + /** + * Reset the position of the cursor to the first row + * + * @return $this + */ + public function reset() + { + $this->cursor = 0; + + return $this; + } + + /** + * Query database and return result + * + * Queries the database with current query and returns the resulted PDOStatement. + * + * If query failed it returns false. It also stores this failed result and to change the query afterwards will not + * change the result. + * + * @return PDOStatement|bool + */ + protected function getStatement() + { + if ($this->result === null) { + $this->result = $this->entityManager->getConnection()->query($this->getQuery()) ?: false; + } + return $this->result; + } +} diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index 46b70f8..eac3b28 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -26,6 +26,7 @@ class QueryBuilder extends Parenthesis implements QueryBuilderInterface { use MakesJoins; + use ExecutesQueries; /** The table to query * @var string */ @@ -67,18 +68,6 @@ class QueryBuilder extends Parenthesis implements QueryBuilderInterface * @var EntityManager */ public static $defaultEntityManager; - /** The result object from PDO - * @var PDOStatement */ - protected $result; - - /** The rows returned - * @var array */ - protected $rows = []; - - /** The position of the cursor - * @var int */ - protected $cursor = 0; - /** @noinspection PhpMissingParentConstructorInspection */ /** * Constructor @@ -293,86 +282,6 @@ public function offset($offset) return $this; } - /** - * Proxy to PDOStatement::setFetchMode() - * - * Please note that this will execute the query - further modifications will not have any effect. - * - * @param int $mode - * @param null $classNameObject - * @param array $ctorarfg - * @return $this - * @see PDOStatement::setFetchMode() - */ - public function setFetchMode($mode, $classNameObject = null, array $ctorarfg = []) - { - $result = $this->getStatement(); - if (!$result) { - return $this; - } - - $result->setFetchMode($mode, $classNameObject, $ctorarfg); - return $this; - } - - /** - * Get the next row from the query result - * - * Please note that this will execute the query - further modifications will not have any effect. - * - * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no more rows - * it returns null. - * - * @return mixed|null - */ - public function one() - { - $result = $this->getStatement(); - if (!$result) { - return null; - } - - $cursor = $this->cursor; - if (!isset($this->rows[$cursor])) { - $this->rows[$cursor] = $result->fetch(); - } - - $this->cursor++; - return $this->rows[$cursor] ?: null; - } - - /** - * Get all rows from the query result - * - * Please note that this will execute the query - further modifications will not have any effect. - * - * If the query fails you should get an exception. Anyway if we couldn't get a result or there are no rows - * it returns an empty array. - * - * @return mixed|null - */ - public function all() - { - $result = []; - while ($next = $this->one()) { - $result[] = $next; - } - - return $result; - } - - /** - * Reset the position of the cursor to the first row - * - * @return $this - */ - public function reset() - { - $this->cursor = 0; - - return $this; - } - /** {@inheritdoc} */ public function getQuery() { @@ -387,24 +296,6 @@ public function getQuery() . ($this->limit ? ' LIMIT ' . $this->limit . ($this->offset ? ' OFFSET ' . $this->offset : '') : ''); } - /** - * Query database and return result - * - * Queries the database with current query and returns the resulted PDOStatement. - * - * If query failed it returns false. It also stores this failed result and to change the query afterwards will not - * change the result. - * - * @return PDOStatement|bool - */ - protected function getStatement() - { - if ($this->result === null) { - $this->result = $this->entityManager->getConnection()->query($this->getQuery()) ?: false; - } - return $this->result; - } - /** {@inheritdoc} */ public function modifier($modifier) { diff --git a/tests/EntityManager/EntityFetcherTest.php b/tests/EntityManager/EntityFetcherTest.php index 1fcfdde..0828e32 100644 --- a/tests/EntityManager/EntityFetcherTest.php +++ b/tests/EntityManager/EntityFetcherTest.php @@ -380,6 +380,16 @@ public function columnsCantBeChanged() self::assertSame('SELECT DISTINCT t0.* FROM "contact_phone" AS t0', $fetcher->getQuery()); } + /** @test */ + public function fetchModeCantBeChanged() + { + $fetcher = $this->em->fetch(ContactPhone::class); + + $this->pdo->shouldNotReceive('query'); + + $fetcher->setFetchMode(\PDO::FETCH_NUM); + } + public function provideJoins() { return [ From 53afb315111044188af0921f4017036aa1bd9757 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Tue, 3 Nov 2020 08:17:57 +0100 Subject: [PATCH 04/16] optimize imports --- src/Dbal/Dbal.php | 1 - src/EntityFetcher.php | 1 - src/EntityManager.php | 1 - src/Event/Saved.php | 1 - src/Event/Saving.php | 1 - src/Observer/CallbackObserver.php | 1 - src/QueryBuilder/QueryBuilder.php | 1 - src/Relation/ManyToMany.php | 2 -- src/Testing/MocksEntityManager.php | 1 - 9 files changed, 10 deletions(-) diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index af1b365..d685bcc 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -8,7 +8,6 @@ use ORM\Exception; use ORM\Exception\NotScalar; use ORM\Exception\UnsupportedDriver; -use ORM\QueryBuilder\QueryBuilder; use PDO; /** diff --git a/src/EntityFetcher.php b/src/EntityFetcher.php index 1190290..d004c52 100644 --- a/src/EntityFetcher.php +++ b/src/EntityFetcher.php @@ -7,7 +7,6 @@ use ORM\QueryBuilder\QueryBuilder; use ORM\QueryBuilder\QueryBuilderInterface; use PDO; -use PDOStatement; /** * Fetch entities from database diff --git a/src/EntityManager.php b/src/EntityManager.php index 6ba0508..24a1b9c 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -13,7 +13,6 @@ use ORM\Exception\InvalidConfiguration; use ORM\Exception\NoConnection; use ORM\Exception\NoEntity; -use ORM\Observer\AbstractObserver; use ORM\Observer\CallbackObserver; use ORM\QueryBuilder\QueryBuilder; use PDO; diff --git a/src/Event/Saved.php b/src/Event/Saved.php index b47c4ff..690cf1e 100644 --- a/src/Event/Saved.php +++ b/src/Event/Saved.php @@ -2,7 +2,6 @@ namespace ORM\Event; -use ORM\Entity; use ORM\Event; /** diff --git a/src/Event/Saving.php b/src/Event/Saving.php index 5bc30ff..784b8a3 100644 --- a/src/Event/Saving.php +++ b/src/Event/Saving.php @@ -2,7 +2,6 @@ namespace ORM\Event; -use ORM\Entity; use ORM\Event; class Saving extends Event diff --git a/src/Observer/CallbackObserver.php b/src/Observer/CallbackObserver.php index 8df9794..a676e3e 100644 --- a/src/Observer/CallbackObserver.php +++ b/src/Observer/CallbackObserver.php @@ -3,7 +3,6 @@ namespace ORM\Observer; use ORM\Event; -use ORM\Exception\InvalidArgument; class CallbackObserver extends AbstractObserver { diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index eac3b28..1ddbc54 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -3,7 +3,6 @@ namespace ORM\QueryBuilder; use ORM\EntityManager; -use PDOStatement; /** * Build a ansi sql query / select statement diff --git a/src/Relation/ManyToMany.php b/src/Relation/ManyToMany.php index f86327f..87dee66 100644 --- a/src/Relation/ManyToMany.php +++ b/src/Relation/ManyToMany.php @@ -7,9 +7,7 @@ use ORM\EntityManager; use ORM\Exception\IncompletePrimaryKey; use ORM\Exception\InvalidRelation; -use ORM\QueryBuilder\QueryBuilder; use ORM\Relation; -use PDO; /** * ManyToMany Relation diff --git a/src/Testing/MocksEntityManager.php b/src/Testing/MocksEntityManager.php index a46d8cc..1d1063b 100644 --- a/src/Testing/MocksEntityManager.php +++ b/src/Testing/MocksEntityManager.php @@ -6,7 +6,6 @@ use ORM\Entity; use ORM\EntityFetcher; use ORM\EntityManager; -use ORM\Exception\IncompletePrimaryKey; use PDO; /** From d641db5bc23a744ce9f989d2157904dd7e426e76 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Tue, 17 Nov 2020 08:34:57 +0100 Subject: [PATCH 05/16] implement public insert interface --- src/BulkInsert.php | 2 +- src/Dbal/Dbal.php | 105 +++++++++++++------ src/Dbal/Escaping.php | 15 --- src/Dbal/Expression.php | 23 ++++ src/Dbal/Mysql.php | 4 +- src/Dbal/Pgsql.php | 4 +- src/Dbal/Sqlite.php | 4 +- src/EM.php | 6 ++ src/EntityManager.php | 14 ++- src/QueryBuilder/ExecutesQueries.php | 8 ++ tests/BulkInsertTest.php | 2 +- tests/Dbal/BasicTest.php | 11 +- tests/Dbal/BulkInsertTest.php | 15 ++- tests/Dbal/DataModificationTest.php | 86 +++++++++++++++ tests/Dbal/EscapeValueTest.php | 9 ++ tests/Dbal/Sqlite/InsertTest.php | 3 +- tests/EntityManager/DataModificationTest.php | 12 ++- tests/EntityManager/GetInstanceTest.php | 10 +- tests/QueryBuilder/BasicTest.php | 19 ++++ tests/TestCase.php | 11 +- 20 files changed, 294 insertions(+), 69 deletions(-) create mode 100644 src/Dbal/Expression.php create mode 100644 src/EM.php create mode 100644 tests/Dbal/DataModificationTest.php diff --git a/src/BulkInsert.php b/src/BulkInsert.php index 5280b6b..661d380 100644 --- a/src/BulkInsert.php +++ b/src/BulkInsert.php @@ -89,7 +89,7 @@ protected function execute() $new = array_splice($this->new, 0, $this->limit); if (!$this->update) { - $success = $this->dbal->insert(...$new); + $success = $this->dbal->insertEntities(...$new); } elseif ($this->useAutoIncrement) { $success = $this->dbal->insertAndSyncWithAutoInc(...$new); } else { diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index d685bcc..874f671 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -9,6 +9,7 @@ use ORM\Exception\NotScalar; use ORM\Exception\UnsupportedDriver; use PDO; +use PDOStatement; /** * Base class for database abstraction @@ -80,6 +81,10 @@ public function setOption($option, $value) */ public function escapeIdentifier($identifier) { + if ($identifier instanceof Expression) { + return (string)$identifier; + } + $quote = $this->quotingCharacter; $divider = $this->identifierDivider; return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote; @@ -94,10 +99,15 @@ public function escapeIdentifier($identifier) */ public function escapeValue($value) { - $type = is_object($value) ? get_class($value) : gettype($value); if ($value instanceof DateTime) { - $type = 'DateTime'; + return $this->escapeDateTime($value); + } + + if ($value instanceof Expression) { + return (string)$value; } + + $type = is_object($value) ? get_class($value) : gettype($value); $method = [ $this, 'escape' . ucfirst($type) ]; if (is_callable($method)) { @@ -117,7 +127,7 @@ public function escapeValue($value) */ public function describe($table) { - throw new UnsupportedDriver('Not supported for this driver'); + throw new UnsupportedDriver('Describe is not supported by this driver'); } /** @@ -141,6 +151,17 @@ protected static function assertSameType(array $entities) return true; } + public function insert($table, array ...$rows) + { + if (count($rows) === 0) { + return 0; + } + + $insert = $this->buildInsert($table, $rows); + $statement = $this->entityManager->getConnection()->query($insert); + return $statement->rowCount(); + } + /** * Insert $entities into database * @@ -148,16 +169,17 @@ protected static function assertSameType(array $entities) * * @param Entity ...$entities * @return bool - * @throws Exception\InvalidArgument */ - public function insert(Entity ...$entities) + public function insertEntities(Entity ...$entities) { if (count($entities) === 0) { return false; } + static::assertSameType($entities); - $insert = $this->buildInsertStatement(...$entities); - $this->entityManager->getConnection()->query($insert); + $this->insert($entities[0]::getTableName(), ...array_map(function (Entity $entity) { + return $entity->getData(); + }, $entities)); return true; } @@ -175,8 +197,8 @@ public function insertAndSync(Entity ...$entities) if (count($entities) === 0) { return false; } - self::assertSameType($entities); - $this->insert(...$entities); + + $this->insertEntities(...$entities); $this->syncInserted(...$entities); return true; } @@ -207,7 +229,7 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) * @return bool * @internal */ - public function update(Entity $entity) + public function updateEntity(Entity $entity) { $data = $entity->getData(); $primaryKey = $entity->getPrimaryKey(); @@ -259,34 +281,27 @@ public function delete(Entity $entity) } /** - * Build the insert statement for $entity + * Build an insert statement for $rows * - * @param Entity $entity - * @param Entity[] $entities + * @param string $table + * @param array $rows * @return string */ - protected function buildInsertStatement(Entity $entity, Entity ...$entities) + protected function buildInsert($table, array $rows) { - array_unshift($entities, $entity); - $cols = []; - $rows = []; - foreach ($entities as $entity) { - $data = $entity->getData(); - $cols = array_unique(array_merge($cols, array_keys($data))); - $rows[] = $data; + // get all columns from rows + $columns = []; + foreach ($rows as $row) { + $columns = array_unique(array_merge($columns, array_keys($row))); } - $cols = array_combine($cols, array_map([$this, 'escapeIdentifier'], $cols)); - - $statement = 'INSERT INTO ' . $this->escapeIdentifier($entity::getTableName()) . ' ' . - '(' . implode(',', $cols) . ') VALUES '; + $statement = 'INSERT INTO ' . $this->escapeIdentifier($table) . ' ' . + '(' . implode(',', array_map([$this, 'escapeIdentifier'], $columns)) . ') VALUES '; - $statement .= implode(',', array_map(function ($values) use ($cols) { - $result = []; - foreach ($cols as $key => $col) { - $result[] = isset($values[$key]) ? $this->escapeValue($values[$key]) : $this->escapeNULL(); - } - return '(' . implode(',', $result) . ')'; + $statement .= implode(',', array_map(function ($values) use ($columns) { + return '(' . implode(',', array_map(function ($column) use ($values) { + return isset($values[$column]) ? $this->escapeValue($values[$column]) : $this->escapeNULL(); + }, $columns)) . ')'; }, $rows)); return $statement; @@ -383,4 +398,32 @@ protected function normalizeType($type) return trim($type); } + + /** + * Extract content from parenthesis in $type + * + * @param string $type + * @return string + */ + protected function extractParenthesis($type) + { + if (preg_match('/\((.+)\)/', $type, $match)) { + return $match[1]; + } + + return null; + } + + protected function convertJoin($join) + { + if (!preg_match('/^JOIN\s+([^\s]+)\s+ON\s+(.*)/ism', $join, $match)) { + throw new Exception\InvalidArgument( + 'Only inner joins with on clause are allowed in update statements' + ); + } + $table = $match[1]; + $condition = $match[2]; + + return [$table, $condition]; + } } diff --git a/src/Dbal/Escaping.php b/src/Dbal/Escaping.php index 0f48530..94b903d 100644 --- a/src/Dbal/Escaping.php +++ b/src/Dbal/Escaping.php @@ -16,21 +16,6 @@ trait Escaping /** @var string */ protected $booleanFalse = '0'; - /** - * Extract content from parenthesis in $type - * - * @param string $type - * @return string - */ - protected function extractParenthesis($type) - { - if (preg_match('/\((.+)\)/', $type, $match)) { - return $match[1]; - } - - return null; - } - /** * Escape a string for query * diff --git a/src/Dbal/Expression.php b/src/Dbal/Expression.php new file mode 100644 index 0000000..361459a --- /dev/null +++ b/src/Dbal/Expression.php @@ -0,0 +1,23 @@ +expression = $expression; + } + + public function __toString() + { + return $this->expression; + } +} diff --git a/src/Dbal/Mysql.php b/src/Dbal/Mysql.php index b9e90b4..6986df7 100644 --- a/src/Dbal/Mysql.php +++ b/src/Dbal/Mysql.php @@ -57,7 +57,9 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) $pKey = $this->escapeIdentifier($entity::getColumnName($entity::getPrimaryKeyVars()[0])); $pdo = $this->entityManager->getConnection(); $pdo->beginTransaction(); - $pdo->query($this->buildInsertStatement(...$entities)); + $pdo->query($this->buildInsert($entities[0]::getTableName(), array_map(function (Entity $entity) { + return $entity->getData(); + }, $entities))); $rows = $pdo->query('SELECT * FROM ' . $table . ' WHERE ' . $pKey . ' >= LAST_INSERT_ID()') ->fetchAll(PDO::FETCH_ASSOC); $pdo->commit(); diff --git a/src/Dbal/Pgsql.php b/src/Dbal/Pgsql.php index 595fb77..42cb8f0 100644 --- a/src/Dbal/Pgsql.php +++ b/src/Dbal/Pgsql.php @@ -52,7 +52,9 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) $entity = reset($entities); $pdo = $this->entityManager->getConnection(); $col = $entity::getColumnName($entity::getPrimaryKeyVars()[0]); - $result = $pdo->query($this->buildInsertStatement(...$entities) . ' RETURNING ' . $col); + $result = $pdo->query($this->buildInsert($entities[0]::getTableName(), array_map(function (Entity $entity) { + return $entity->getData(); + }, $entities)) . ' RETURNING ' . $col); while ($id = $result->fetchColumn()) { $this->updateAutoincrement($entity, $id); $entity = next($entity); diff --git a/src/Dbal/Sqlite.php b/src/Dbal/Sqlite.php index ccfa940..7386fe5 100644 --- a/src/Dbal/Sqlite.php +++ b/src/Dbal/Sqlite.php @@ -48,7 +48,9 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) $table = $this->escapeIdentifier($entity::getTableName()); $pKey = $this->escapeIdentifier($entity::getColumnName($entity::getPrimaryKeyVars()[0])); $pdo->beginTransaction(); - $pdo->query($this->buildInsertStatement(...$entities)); + $pdo->query($this->buildInsert($entities[0]::getTableName(), array_map(function (Entity $entity) { + return $entity->getData(); + }, $entities))); $rows = $pdo->query('SELECT * FROM ' . $table . ' WHERE ' . $pKey . ' <= ' . $pdo->lastInsertId() . ' ORDER BY ' . $pKey . ' DESC LIMIT ' . count($entities)) ->fetchAll(PDO::FETCH_ASSOC); diff --git a/src/EM.php b/src/EM.php new file mode 100644 index 0000000..de1e21f --- /dev/null +++ b/src/EM.php @@ -0,0 +1,6 @@ +dbal, Article::class))->limit(2)->noUpdates(); $articles = [new Article, new Article]; - $this->dbal->shouldReceive('insert')->with(...$articles) + $this->dbal->shouldReceive('insertEntities')->with(...$articles) ->once()->andReturn(true); $bulk->add(...$articles); diff --git a/tests/Dbal/BasicTest.php b/tests/Dbal/BasicTest.php index 5bbb365..819ee0a 100644 --- a/tests/Dbal/BasicTest.php +++ b/tests/Dbal/BasicTest.php @@ -3,6 +3,7 @@ namespace ORM\Test\Dbal; use ORM\Dbal; +use ORM\EM; use ORM\Exception; use ORM\Test\TestCase; @@ -34,11 +35,19 @@ public function escapesSchemasInIdentifiers() self::assertSame('"user"."id"', $escaped); } + /** @test */ + public function doesNotEscapeExpressions() + { + $escaped = $this->dbal->escapeIdentifier(EM::raw('DATE("column")')); + + self::assertSame('DATE("column")', $escaped); + } + /** @test */ public function doesNotSupportDescribe() { self::expectException(Exception::class); - self::expectExceptionMessage('Not supported for this driver'); + self::expectExceptionMessage('Describe is not supported by this driver'); $this->dbal->describe('any'); } diff --git a/tests/Dbal/BulkInsertTest.php b/tests/Dbal/BulkInsertTest.php index 93b1c93..f8c047a 100644 --- a/tests/Dbal/BulkInsertTest.php +++ b/tests/Dbal/BulkInsertTest.php @@ -3,6 +3,7 @@ namespace ORM\Test\Dbal; use Mockery as m; +use ORM\Dbal\Dbal; use ORM\Dbal\Mysql; use ORM\Dbal\Other; use ORM\Dbal\Pgsql; @@ -30,9 +31,10 @@ public function provideDrivers() * @test */ public function insertReturnsFalseWithoutEntities($driver) { + /** @var Dbal $dbal */ $dbal = new $driver($this->em); - self::assertFalse($dbal->insert()); + self::assertFalse($dbal->insertEntities()); self::assertFalse($dbal->insertAndSync()); self::assertFalse($dbal->insertAndSyncWithAutoInc()); } @@ -42,12 +44,13 @@ public function insertReturnsFalseWithoutEntities($driver) * @test */ public function insertThrowsWhenTypesDiffer($driver) { + /** @var Dbal $dbal */ $dbal = new $driver($this->em); self::expectException(InvalidArgument::class); self::expectExceptionMessage('$entities[1] is not from the same type'); - self::assertFalse($dbal->insert(new Article, new Category)); + self::assertFalse($dbal->insertEntities(new Article, new Category)); self::assertFalse($dbal->insertAndSync(new Article, new Category)); self::assertFalse($dbal->insertAndSyncWithAutoInc(new Article, new Category)); } @@ -62,9 +65,10 @@ public function bulkInsertWithoutSync() $this->pdo->shouldReceive('query')->with(m::pattern( '/INSERT INTO .* \("id","text"\) VALUES \(23,\'foo\'\),\(42,NULL\)/' - ))->once()->andReturn(m::mock(\PDOStatement::class)); + ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(2); - $this->dbal->insert(...$articles); + $this->dbal->insertEntities(...$articles); } /** @test */ @@ -77,7 +81,8 @@ public function bulkInsertWithCompositePrimaryKey() $this->pdo->shouldReceive('query')->with(m::pattern( '/INSERT INTO .* \("id","name","number"\) VALUES \(.*\),\(.*\)/' - ))->once()->andReturn(m::mock(\PDOStatement::class)); + ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(2); $this->pdo->shouldReceive('query')->with(m::pattern( '/SELECT \* FROM .* WHERE \("id","name"\) IN \((VALUES )?(\(.*\))(,\(.*\))*\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); diff --git a/tests/Dbal/DataModificationTest.php b/tests/Dbal/DataModificationTest.php new file mode 100644 index 0000000..b6a614d --- /dev/null +++ b/tests/Dbal/DataModificationTest.php @@ -0,0 +1,86 @@ +pdo->shouldReceive('query')->with( + "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo','bar')" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->dbal->insert('examples', ['col1' => 'foo', 'col2' => 'bar']); + } + + /** @test */ + public function insertsEscapedValues() + { + $this->pdo->shouldReceive('query')->with( + "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('hempel\\'s sofa','2020-11-12T08:42:00.000000Z')" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->dbal->insert('examples', ['col1' => 'hempel\'s sofa', 'col2' => new \DateTime('2020-11-12 08:42:00')]); + } + + /** @test */ + public function insertsAllColumnsFromAllRows() + { + $this->pdo->shouldReceive('query')->with( + "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo',NULL),(NULL,'bar')" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(2); + + $this->dbal->insert('examples', ['col1' => 'foo'], ['col2' => 'bar']); + } + + /** @test */ + public function insertReturns0IfNoRowsAreGiven() + { + $result = $this->dbal->insert('examples'); + + self::assertSame(0, $result); + } + + /** @test */ + public function insertReturnsTheNumberOfAffectedRows() + { + $this->pdo->shouldReceive('query')->with( + "INSERT INTO \"examples\" (\"col1\",\"col2\") VALUES ('foo',NULL),(NULL,'bar')" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(2); + + $result = $this->dbal->insert('examples', ['col1' => 'foo'], ['col2' => 'bar']); + + self::assertSame(2, $result); + } + +// /** @test */ +// public function executesAnUpdateStatement() +// { +// $this->pdo->shouldReceive('query')->with( +// "UPDATE \"examples\" SET \"foo\" = 'bar' WHERE \"id\" = 42" +// )->once()->andReturn($statement = m::mock(PDOStatement::class)); +// $statement->shouldReceive('rowCount')->andReturn(1); +// +// $this->dbal->update('examples', ['id' => 42], ['foo' => 'bar']); +// } +// +// /** @test */ +// public function updatesEscapedValues() +// { +// $this->pdo->shouldReceive('query')->with( +// "UPDATE \"examples\" SET \"col1\" = 'hempel\\'s sofa',\"col2\" = 23 WHERE \"id\" = 42" +// )->once()->andReturn($statement = m::mock(PDOStatement::class)); +// $statement->shouldReceive('rowCount')->andReturn(1); +// +// $this->dbal->update('examples', ['id' => 42], ['col1' => 'hempel\'s sofa', 'col2' => 23]); +// } +} diff --git a/tests/Dbal/EscapeValueTest.php b/tests/Dbal/EscapeValueTest.php index 86af128..98c3511 100644 --- a/tests/Dbal/EscapeValueTest.php +++ b/tests/Dbal/EscapeValueTest.php @@ -3,6 +3,7 @@ namespace ORM\Test\Dbal; use ORM\Dbal; +use ORM\EM; use ORM\Exception\NotScalar; use ORM\Test\Examples\DateTimeDerivate; use ORM\Test\TestCase; @@ -68,6 +69,14 @@ public function booleanUseDefaults($value, $expected) self::assertSame($expected, $result); } + /** @test */ + public function doesNotConvertExpressions() + { + $result = $this->dbal->escapeValue(EM::raw('DATE(NOW())')); + + self::assertSame('DATE(NOW())', $result); + } + /** @test */ public function dateTime() { diff --git a/tests/Dbal/Sqlite/InsertTest.php b/tests/Dbal/Sqlite/InsertTest.php index 17f7225..87ce898 100644 --- a/tests/Dbal/Sqlite/InsertTest.php +++ b/tests/Dbal/Sqlite/InsertTest.php @@ -26,7 +26,8 @@ public function buildsValidQueryForCompositeKeys() $this->pdo->shouldReceive('query')->with(m::pattern( '/INSERT INTO .* \("id","name","number"\) VALUES \(.*\)/' - ))->once()->andReturn(m::mock(\PDOStatement::class)); + ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); $this->pdo->shouldReceive('query')->with(m::pattern( '/SELECT \* FROM .* WHERE \("id","name"\) IN \((\(.*\))(,\(.*\))*\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); diff --git a/tests/EntityManager/DataModificationTest.php b/tests/EntityManager/DataModificationTest.php index 8ccdd10..4112250 100644 --- a/tests/EntityManager/DataModificationTest.php +++ b/tests/EntityManager/DataModificationTest.php @@ -215,7 +215,8 @@ public function insertReturnsTrue() $this->pdo->shouldReceive('query') ->with('INSERT INTO "psr0_studly_caps" ("id","foo") VALUES (42,\'bar\')') - ->once()->andReturn(m::mock(\PDOStatement::class)); + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); $this->pdo->shouldReceive('query')->with('SELECT * FROM "psr0_studly_caps" WHERE "id" IN (42)') ->once()->andReturn($statement = m::mock(\PDOStatement::class)); $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); @@ -247,7 +248,8 @@ public function doesNotUseAutoIncrement($driver) $this->pdo->shouldReceive('getAttribute')->with(\PDO::ATTR_DRIVER_NAME) ->atLeast()->once()->andReturn($driver); $this->pdo->shouldReceive('query')->with(m::pattern('/^INSERT INTO .* VALUES/')) - ->once()->andReturn(m::mock(\PDOStatement::class)); + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); $this->pdo->shouldReceive('query')->with(m::pattern('/^SELECT \* FROM .* WHERE .* IN (.*)/')) ->once()->andReturn($statement = m::mock(\PDOStatement::class)); $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); @@ -371,9 +373,11 @@ public function updateStatement($entity, $statement) /** @dataProvider provideUpdateStatements * @test */ - public function updateReturnsSuccessAndSyncs($entity, $statement) + public function updateReturnsSuccessAndSyncs($entity, $query) { - $this->pdo->shouldReceive('query')->with($statement)->once()->andReturn(m::mock(\PDOStatement::class)); + $statement = m::mock(\PDOStatement::class); + $statement->shouldReceive('rowCount')->andReturn(1); + $this->pdo->shouldReceive('query')->with($query)->once()->andReturn($statement); $this->em->shouldReceive('sync')->with($entity, true)->once()->andReturn(true); $result = $this->em->update($entity); diff --git a/tests/EntityManager/GetInstanceTest.php b/tests/EntityManager/GetInstanceTest.php index b95e6dd..9e8dc1b 100644 --- a/tests/EntityManager/GetInstanceTest.php +++ b/tests/EntityManager/GetInstanceTest.php @@ -3,6 +3,7 @@ namespace ORM\Test\EntityManager; use Mockery as m; +use ORM\EM; use ORM\EntityManager; use ORM\Test\EntityManager\Examples\Concrete; use ORM\Test\EntityManager\Examples\Entity; @@ -11,7 +12,6 @@ class GetInstanceTest extends TestCase { - /** @test */ public function returnsTheLastCreatedEntityManager() { @@ -77,4 +77,12 @@ public function usesTheResolverProvided() self::assertSame($em, EntityManager::getInstance(Concrete::class)); } + + /** @test */ + public function emIsAnAliasForEntityManager() + { + $result = EM::getInstance(); + + self::assertInstanceOf(EntityManager::class, $result); + } } diff --git a/tests/QueryBuilder/BasicTest.php b/tests/QueryBuilder/BasicTest.php index da30bcf..f011288 100644 --- a/tests/QueryBuilder/BasicTest.php +++ b/tests/QueryBuilder/BasicTest.php @@ -2,6 +2,8 @@ namespace ORM\Test\QueryBuilder; +use Mockery as m; +use ORM\Dbal\Expression; use ORM\QueryBuilder\QueryBuilder; use ORM\Test\TestCase; @@ -205,4 +207,21 @@ public function modifier() self::assertSame('SELECT DISTINCT SQL_NO_CACHE * FROM foobar', $query->getQuery()); self::assertSame($query, $result); } + + /** @test */ + public function executesDbalUpdate() + { + $query = new QueryBuilder('foo', '', $this->em); + $query->where('foo.id', 42); + $query->join('bar', 'foo.barId = bar.id'); + + $this->dbal->shouldReceive('update')->with( + m::type(Expression::class), + ['foo.id = 42'], + ['col1' => 'value'], + ['JOIN bar ON foo.barId = bar.id'] + )->once()->andReturn(1); + + $query->update(['col1' => 'value']); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index ef4e035..c0ddc5a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace ORM\Test; +use Mockery as m; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery\Mock; use ORM\Dbal; @@ -31,7 +32,7 @@ protected function setUp() TestEntity::resetStaticsForTest(); TestEntityManager::resetStaticsForTest(); - $this->mocks['pdo'] = $this->pdo = \Mockery::mock(\PDO::class); + $this->pdo = $this->mocks['pdo'] = m::mock(\PDO::class); $this->pdo->shouldReceive('quote')->andReturnUsing(function ($var) { return '\'' . addslashes($var) . '\''; })->byDefault(); @@ -41,10 +42,10 @@ protected function setUp() $this->pdo->shouldReceive('getAttribute')->with(\PDO::ATTR_DRIVER_NAME)->andReturn('mssql')->byDefault(); $this->pdo->shouldReceive('lastInsertId')->andReturn('666')->byDefault(); - $this->mocks['em'] = $this->em = \Mockery::mock(TestEntityManager::class, [])->makePartial(); + $this->em = $this->mocks['em'] = m::mock(TestEntityManager::class, [])->makePartial(); $this->em->shouldReceive('getConnection')->andReturn($this->pdo)->byDefault(); - $this->mocks['dbal'] = $this->dbal = \Mockery::mock(Dbal\Mysql::class, [$this->em])->makePartial(); + $this->dbal = $this->mocks['dbal'] = m::mock(Dbal\Mysql::class, [$this->em])->makePartial(); $this->em->shouldReceive('getDbal')->andReturn($this->dbal)->byDefault(); QueryBuilder::$defaultEntityManager = $this->em; @@ -64,7 +65,7 @@ protected function assertPostConditions() protected function addMockeryExpectationsToAssertionCount() { - $container = \Mockery::getContainer(); + $container = m::getContainer(); if ($container != null) { $count = $container->mockery_getExpectationCount(); $this->addToAssertionCount($count); @@ -73,7 +74,7 @@ protected function addMockeryExpectationsToAssertionCount() protected function closeMockery() { - \Mockery::close(); + m::close(); } protected static function setProtectedProperty($object, $property, $value) From 897c71c290abfeb85f14533a01e28d500d693d5e Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Sun, 22 Nov 2020 10:25:30 +0100 Subject: [PATCH 06/16] implement public update interface --- .gitignore | 1 + src/Dbal/Dbal.php | 150 +++++++----------- src/Dbal/Escaping.php | 45 ++++++ src/Dbal/Mysql.php | 12 ++ src/Dbal/Other.php | 2 + src/Dbal/Pgsql.php | 10 ++ .../QueryLanguage/UpdateFromStatement.php | 38 +++++ .../QueryLanguage/UpdateJoinStatement.php | 16 ++ src/Dbal/QueryLanguage/UpdateStatement.php | 35 ++++ src/Dbal/Sqlite.php | 10 ++ src/EntityManager.php | 2 +- src/QueryBuilder/ExecutesQueries.php | 21 +++ tests/Dbal/BasicTest.php | 14 ++ tests/Dbal/DataModificationTest.php | 23 +-- tests/Dbal/Mysql/UpdateJoinTest.php | 34 ++++ tests/Dbal/Other/UpdateTest.php | 38 +++++ tests/Dbal/Pgsql/UpdateFromTest.php | 48 ++++++ tests/Dbal/Sqlite/InsertTest.php | 2 +- tests/Dbal/Sqlite/UpdateFromTest.php | 34 ++++ tests/TestCase.php | 8 +- 20 files changed, 424 insertions(+), 119 deletions(-) create mode 100644 src/Dbal/QueryLanguage/UpdateFromStatement.php create mode 100644 src/Dbal/QueryLanguage/UpdateJoinStatement.php create mode 100644 src/Dbal/QueryLanguage/UpdateStatement.php create mode 100644 tests/Dbal/Mysql/UpdateJoinTest.php create mode 100644 tests/Dbal/Other/UpdateTest.php create mode 100644 tests/Dbal/Pgsql/UpdateFromTest.php create mode 100644 tests/Dbal/Sqlite/UpdateFromTest.php diff --git a/.gitignore b/.gitignore index 611921e..f34145b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /composer.lock /docs/.jekyll-cache /examples/*.sqlite +/examples/*.sql diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index 874f671..7808902 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -2,14 +2,12 @@ namespace ORM\Dbal; -use DateTime; +use ORM\Dbal\QueryLanguage\UpdateStatement; use ORM\Entity; use ORM\EntityManager; use ORM\Exception; -use ORM\Exception\NotScalar; use ORM\Exception\UnsupportedDriver; use PDO; -use PDOStatement; /** * Base class for database abstraction @@ -20,6 +18,7 @@ abstract class Dbal { use Escaping; + use UpdateStatement; /** @var array */ protected static $typeMapping = []; @@ -73,50 +72,6 @@ public function setOption($option, $value) return $this; } - /** - * Returns $identifier quoted for use in a sql statement - * - * @param string $identifier Identifier to quote - * @return string - */ - public function escapeIdentifier($identifier) - { - if ($identifier instanceof Expression) { - return (string)$identifier; - } - - $quote = $this->quotingCharacter; - $divider = $this->identifierDivider; - return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote; - } - - /** - * Returns $value formatted to use in a sql statement. - * - * @param mixed $value The variable that should be returned in SQL syntax - * @return string - * @throws NotScalar - */ - public function escapeValue($value) - { - if ($value instanceof DateTime) { - return $this->escapeDateTime($value); - } - - if ($value instanceof Expression) { - return (string)$value; - } - - $type = is_object($value) ? get_class($value) : gettype($value); - $method = [ $this, 'escape' . ucfirst($type) ]; - - if (is_callable($method)) { - return call_user_func($method, $value); - } else { - throw new NotScalar('$value has to be scalar data type. ' . gettype($value) . ' given'); - } - } - /** * Describe a table * @@ -130,27 +85,6 @@ public function describe($table) throw new UnsupportedDriver('Describe is not supported by this driver'); } - /** - * @param Entity[] $entities - * @return bool - * @throws Exception\InvalidArgument - */ - protected static function assertSameType(array $entities) - { - if (count($entities) < 2) { - return true; - } - - $type = get_class(reset($entities)); - foreach ($entities as $i => $entity) { - if (get_class($entity) !== $type) { - throw new Exception\InvalidArgument(sprintf('$entities[%d] is not from the same type', $i)); - } - } - - return true; - } - public function insert($table, array ...$rows) { if (count($rows) === 0) { @@ -222,6 +156,37 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) throw new UnsupportedDriver('Auto incremented column for this driver is not supported'); } + /** + * Update $table using $where to set $updates + * + * Simple usage: `update('table', ['id' => 23], ['name' => 'John Doe'])` + * + * For advanced queries with parenthesis, joins (if supported from your DBMS) etc. use QueryBuilder: + * + * ```php + * $em->query('table') + * ->where('birth_date', '>', EM::raw('DATE_SUB(NOW(), INTERVAL 18 YEARS)')) + * ->update(['teenager' => true]); + * ``` + * + * @param string $table The table to update + * @param array $where An array of where conditions + * @param array $updates An array of columns to update + * @param array $joins For internal use from query builder only + * @return int The number of affected rows + * @throws UnsupportedDriver + */ + public function update($table, array $where, array $updates, array $joins = []) + { + if (!empty($joins)) { + throw new UnsupportedDriver('Updates with joins are not supported by this driver'); + } + + $query = $this->buildUpdateStatement($table, $where, $updates); + $statement = $this->entityManager->getConnection()->query($query); + return $statement->rowCount(); + } + /** * Update $entity in database and returns success * @@ -236,23 +201,14 @@ public function updateEntity(Entity $entity) $where = []; foreach ($primaryKey as $attribute => $value) { - $col = $entity::getColumnName($attribute); - $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value); + $col = $entity::getColumnName($attribute); + $where[$col] = $value; if (isset($data[$col])) { unset($data[$col]); } } - $set = []; - foreach ($data as $col => $value) { - $set[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value); - } - - $statement = 'UPDATE ' . $this->escapeIdentifier($entity::getTableName()) . ' ' . - 'SET ' . implode(',', $set) . ' ' . - 'WHERE ' . implode(' AND ', $where); - $this->entityManager->getConnection()->query($statement); - + $this->update($entity::getTableName(), $where, $data); return $this->entityManager->sync($entity, true); } @@ -280,6 +236,27 @@ public function delete(Entity $entity) return true; } + /** + * @param Entity[] $entities + * @return bool + * @throws Exception\InvalidArgument + */ + protected static function assertSameType(array $entities) + { + if (count($entities) < 2) { + return true; + } + + $type = get_class(reset($entities)); + foreach ($entities as $i => $entity) { + if (get_class($entity) !== $type) { + throw new Exception\InvalidArgument(sprintf('$entities[%d] is not from the same type', $i)); + } + } + + return true; + } + /** * Build an insert statement for $rows * @@ -413,17 +390,4 @@ protected function extractParenthesis($type) return null; } - - protected function convertJoin($join) - { - if (!preg_match('/^JOIN\s+([^\s]+)\s+ON\s+(.*)/ism', $join, $match)) { - throw new Exception\InvalidArgument( - 'Only inner joins with on clause are allowed in update statements' - ); - } - $table = $match[1]; - $condition = $match[2]; - - return [$table, $condition]; - } } diff --git a/src/Dbal/Escaping.php b/src/Dbal/Escaping.php index 94b903d..c7d66b1 100644 --- a/src/Dbal/Escaping.php +++ b/src/Dbal/Escaping.php @@ -4,6 +4,7 @@ use DateTime; use DateTimeZone; +use ORM\Exception\NotScalar; trait Escaping { @@ -16,6 +17,50 @@ trait Escaping /** @var string */ protected $booleanFalse = '0'; + /** + * Returns $identifier quoted for use in a sql statement + * + * @param string $identifier Identifier to quote + * @return string + */ + public function escapeIdentifier($identifier) + { + if ($identifier instanceof Expression) { + return (string)$identifier; + } + + $quote = $this->quotingCharacter; + $divider = $this->identifierDivider; + return $quote . str_replace($divider, $quote . $divider . $quote, $identifier) . $quote; + } + + /** + * Returns $value formatted to use in a sql statement. + * + * @param mixed $value The variable that should be returned in SQL syntax + * @return string + * @throws NotScalar + */ + public function escapeValue($value) + { + if ($value instanceof DateTime) { + return $this->escapeDateTime($value); + } + + if ($value instanceof Expression) { + return (string)$value; + } + + $type = is_object($value) ? get_class($value) : gettype($value); + $method = [ $this, 'escape' . ucfirst($type) ]; + + if (is_callable($method)) { + return call_user_func($method, $value); + } else { + throw new NotScalar('$value has to be scalar data type. ' . gettype($value) . ' given'); + } + } + /** * Escape a string for query * diff --git a/src/Dbal/Mysql.php b/src/Dbal/Mysql.php index 6986df7..2ebeb36 100644 --- a/src/Dbal/Mysql.php +++ b/src/Dbal/Mysql.php @@ -2,6 +2,7 @@ namespace ORM\Dbal; +use ORM\Dbal\QueryLanguage\UpdateJoinStatement; use ORM\Entity; use ORM\Exception; use PDO; @@ -15,6 +16,8 @@ */ class Mysql extends Dbal { + use UpdateJoinStatement; + protected static $typeMapping = [ 'tinyint' => Type\Number::class, 'smallint' => Type\Number::class, @@ -90,6 +93,15 @@ public function describe($table) return new Table($cols); } + public function update($table, array $where, array $updates, array $joins = []) + { + $query = $this->buildUpdateJoinStatement($table, $where, $updates, $joins); +// echo $query; +// return 0; + $statement = $this->entityManager->getConnection()->query($query); + return $statement->rowCount(); + } + /** * Normalize a column definition * diff --git a/src/Dbal/Other.php b/src/Dbal/Other.php index eb8bf3c..c1bda4d 100644 --- a/src/Dbal/Other.php +++ b/src/Dbal/Other.php @@ -2,6 +2,8 @@ namespace ORM\Dbal; +use ORM\Exception\UnsupportedDriver; + /** * Database abstraction for other databases * diff --git a/src/Dbal/Pgsql.php b/src/Dbal/Pgsql.php index 42cb8f0..4d15224 100644 --- a/src/Dbal/Pgsql.php +++ b/src/Dbal/Pgsql.php @@ -2,6 +2,7 @@ namespace ORM\Dbal; +use ORM\Dbal\QueryLanguage\UpdateFromStatement; use ORM\Entity; use ORM\Exception; use ORM\QueryBuilder\QueryBuilder; @@ -15,6 +16,8 @@ */ class Pgsql extends Dbal { + use UpdateFromStatement; + protected static $typeMapping = [ 'integer' => Type\Number::class, 'smallint' => Type\Number::class, @@ -63,6 +66,13 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) return true; } + public function update($table, array $where, array $updates, array $joins = []) + { + $query = $this->buildUpdateFromStatement($table, $where, $updates, $joins); + $statement = $this->entityManager->getConnection()->query($query); + return $statement->rowCount(); + } + public function describe($schemaTable) { $table = explode($this->identifierDivider, $schemaTable); diff --git a/src/Dbal/QueryLanguage/UpdateFromStatement.php b/src/Dbal/QueryLanguage/UpdateFromStatement.php new file mode 100644 index 0000000..c251bc8 --- /dev/null +++ b/src/Dbal/QueryLanguage/UpdateFromStatement.php @@ -0,0 +1,38 @@ +convertJoin(array_shift($joins)); + $fromClause = ' FROM ' . $fromTable . + (!empty($joins) ? ' ' . implode(' ', $joins) : ''); + array_unshift($where, $condition); + } + + return 'UPDATE ' . $this->escapeIdentifier($table) . + $this->buildSetClause($updates) . + $fromClause . + $this->buildWhereClause($where); + } + + protected function convertJoin($join) + { + if (!preg_match('/^JOIN\s+([^\s]+)\s+ON\s+(.*)/ism', $join, $match)) { + throw new Exception\InvalidArgument( + 'Only inner joins with on clause are allowed in update from statements' + ); + } + $table = $match[1]; + $condition = $match[2]; + + return [$table, $condition]; + } +} diff --git a/src/Dbal/QueryLanguage/UpdateJoinStatement.php b/src/Dbal/QueryLanguage/UpdateJoinStatement.php new file mode 100644 index 0000000..ed4f8ef --- /dev/null +++ b/src/Dbal/QueryLanguage/UpdateJoinStatement.php @@ -0,0 +1,16 @@ +escapeIdentifier($table) . + (!empty($joins) ? ' ' . implode(' ', $joins) : '') . + $this->buildSetClause($updates) . + $this->buildWhereClause($where); + } +} diff --git a/src/Dbal/QueryLanguage/UpdateStatement.php b/src/Dbal/QueryLanguage/UpdateStatement.php new file mode 100644 index 0000000..61139a0 --- /dev/null +++ b/src/Dbal/QueryLanguage/UpdateStatement.php @@ -0,0 +1,35 @@ +escapeIdentifier($table) . + $this->buildSetClause($updates) . + $this->buildWhereClause($where); + } + + protected function buildSetClause(array $updates) + { + return ' SET ' . implode(',', array_map(function ($column, $value) { + return $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($value); + }, array_keys($updates), $updates)); + } + + protected function buildWhereClause(array $where) + { + $whereClause = !empty($where) ? ' WHERE ' : ''; + $i = 0; + foreach ($where as $column => $condition) { + if ($i > 0 && (!is_numeric($column) || !preg_match('/^\s*(AND|OR)/i', $condition))) { + $whereClause .= ' AND '; + } + $whereClause .= !is_numeric($column) ? + $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($condition) : $condition; + $i++; + } + return $whereClause; + } +} diff --git a/src/Dbal/Sqlite.php b/src/Dbal/Sqlite.php index 7386fe5..71585b1 100644 --- a/src/Dbal/Sqlite.php +++ b/src/Dbal/Sqlite.php @@ -2,6 +2,7 @@ namespace ORM\Dbal; +use ORM\Dbal\QueryLanguage\UpdateFromStatement; use ORM\Entity; use ORM\Exception; use PDO; @@ -14,6 +15,8 @@ */ class Sqlite extends Dbal { + use UpdateFromStatement; + protected static $typeMapping = [ 'integer' => Type\Number::class, 'int' => Type\Number::class, @@ -66,6 +69,13 @@ public function insertAndSyncWithAutoInc(Entity ...$entities) return true; } + public function update($table, array $where, array $updates, array $joins = []) + { + $query = $this->buildUpdateFromStatement($table, $where, $updates, $joins); + $statement = $this->entityManager->getConnection()->query($query); + return $statement->rowCount(); + } + public function describe($schemaTable) { $table = explode($this->identifierDivider, $schemaTable); diff --git a/src/EntityManager.php b/src/EntityManager.php index 6cf9eea..e73a96f 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -480,7 +480,7 @@ public function finishBulkInserts($class) */ public function update(Entity $entity) { - return $this->getDbal()->update($entity); + return $this->getDbal()->updateEntity($entity); } /** diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php index 794a4af..19fd2a1 100644 --- a/src/QueryBuilder/ExecutesQueries.php +++ b/src/QueryBuilder/ExecutesQueries.php @@ -106,6 +106,27 @@ public function reset() return $this; } + /** + * Execute an update statement for the current query + * + * **NOTE:** not all drivers support UPDATE with JOIN (or FROM). Has to be implemented in the database abstraction + * layer. + * + * $updates should be an array which columns to update with what value. Use expressions to bypass escaping. + * + * @param array $updates An array of columns to update + * @return int The number of affected rows + */ + public function update(array $updates) + { + return $this->entityManager->getDbal()->update( + EntityManager::raw($this->tableName . ($this->alias ? ' AS ' . $this->alias : '')), + $this->where, + $updates, + $this->joins + ); + } + /** * Query database and return result * diff --git a/tests/Dbal/BasicTest.php b/tests/Dbal/BasicTest.php index 819ee0a..edb2e2f 100644 --- a/tests/Dbal/BasicTest.php +++ b/tests/Dbal/BasicTest.php @@ -51,4 +51,18 @@ public function doesNotSupportDescribe() $this->dbal->describe('any'); } + + + + /** @test */ + public function doesNotSupportUpdateWithJoins() + { + self::expectException(Exception::class); + self::expectExceptionMessage('Updates with joins are not supported by this driver'); + + $this->em->query('examples') + ->join('names', 'exampleId = examples.id') + ->where('examples.id', 42) + ->update(['foo' => EM::raw('names.foo')]); + } } diff --git a/tests/Dbal/DataModificationTest.php b/tests/Dbal/DataModificationTest.php index b6a614d..4aa88a0 100644 --- a/tests/Dbal/DataModificationTest.php +++ b/tests/Dbal/DataModificationTest.php @@ -3,6 +3,7 @@ namespace ORM\Test\Dbal; use Mockery as m; +use ORM\EM; use ORM\Test\TestCase; use PDOStatement; @@ -61,26 +62,4 @@ public function insertReturnsTheNumberOfAffectedRows() self::assertSame(2, $result); } - -// /** @test */ -// public function executesAnUpdateStatement() -// { -// $this->pdo->shouldReceive('query')->with( -// "UPDATE \"examples\" SET \"foo\" = 'bar' WHERE \"id\" = 42" -// )->once()->andReturn($statement = m::mock(PDOStatement::class)); -// $statement->shouldReceive('rowCount')->andReturn(1); -// -// $this->dbal->update('examples', ['id' => 42], ['foo' => 'bar']); -// } -// -// /** @test */ -// public function updatesEscapedValues() -// { -// $this->pdo->shouldReceive('query')->with( -// "UPDATE \"examples\" SET \"col1\" = 'hempel\\'s sofa',\"col2\" = 23 WHERE \"id\" = 42" -// )->once()->andReturn($statement = m::mock(PDOStatement::class)); -// $statement->shouldReceive('rowCount')->andReturn(1); -// -// $this->dbal->update('examples', ['id' => 42], ['col1' => 'hempel\'s sofa', 'col2' => 23]); -// } } diff --git a/tests/Dbal/Mysql/UpdateJoinTest.php b/tests/Dbal/Mysql/UpdateJoinTest.php new file mode 100644 index 0000000..0449e4b --- /dev/null +++ b/tests/Dbal/Mysql/UpdateJoinTest.php @@ -0,0 +1,34 @@ +dbal = new Mysql($this->em); + } + + /** @test */ + public function executesAnUpdateJoinStatement() + { + $this->pdo->shouldReceive('query')->with( + "UPDATE examples JOIN names ON exampleId = examples.id SET \"foo\" = names.foo WHERE examples.id = 42" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->em->query('examples') + ->join('names', 'exampleId = examples.id') + ->where('examples.id', 42) + ->update(['foo' => EM::raw('names.foo')]); + } +} diff --git a/tests/Dbal/Other/UpdateTest.php b/tests/Dbal/Other/UpdateTest.php new file mode 100644 index 0000000..8e8da7e --- /dev/null +++ b/tests/Dbal/Other/UpdateTest.php @@ -0,0 +1,38 @@ +dbal = new Other($this->em); + } + + /** @test */ + public function executesAnUpdateStatement() + { + $this->pdo->shouldReceive('query')->with( + "UPDATE \"examples\" SET \"foo\" = 'bar' WHERE \"id\" = 42" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->dbal->update('examples', ['id' => 42], ['foo' => 'bar']); + } + + /** @test */ + public function updatesEscapedValues() + { + $this->pdo->shouldReceive('query')->with( + "UPDATE \"examples\" SET \"col1\" = 'hempel\\'s sofa',\"col2\" = 23 WHERE \"id\" = 42" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->dbal->update('examples', ['id' => 42], ['col1' => 'hempel\'s sofa', 'col2' => 23]); + } +} diff --git a/tests/Dbal/Pgsql/UpdateFromTest.php b/tests/Dbal/Pgsql/UpdateFromTest.php new file mode 100644 index 0000000..2ec90a1 --- /dev/null +++ b/tests/Dbal/Pgsql/UpdateFromTest.php @@ -0,0 +1,48 @@ +dbal = new Pgsql($this->em); + } + + /** @test */ + public function executesAnUpdateFromStatement() + { + $this->pdo->shouldReceive('query')->with( + "UPDATE examples SET \"foo\" = names.foo FROM names WHERE exampleId = examples.id AND examples.id = 42" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->em->query('examples') + ->join('names', 'exampleId = examples.id') + ->where('examples.id', 42) + ->update(['foo' => EM::raw('names.foo')]); + } + + /** @test */ + public function updateFromOnlyWorksWithInnerJoins() + { + self::expectException(Exception::class); + self::expectExceptionMessage('Only inner joins with on clause are allowed in update from statements'); + + + $this->em->query('examples') + ->leftJoin('names', 'exampleId = examples.id') + ->where('examples.id', 42) + ->update(['foo' => EM::raw('names.foo')]); + } +} diff --git a/tests/Dbal/Sqlite/InsertTest.php b/tests/Dbal/Sqlite/InsertTest.php index 87ce898..9a6c3a2 100644 --- a/tests/Dbal/Sqlite/InsertTest.php +++ b/tests/Dbal/Sqlite/InsertTest.php @@ -29,7 +29,7 @@ public function buildsValidQueryForCompositeKeys() ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); $statement->shouldReceive('rowCount')->andReturn(1); $this->pdo->shouldReceive('query')->with(m::pattern( - '/SELECT \* FROM .* WHERE \("id","name"\) IN \((\(.*\))(,\(.*\))*\)/' + '/SELECT \* FROM .* WHERE \("id","name"\) IN \(VALUES (\(.*\))(,\(.*\))*\)/' ))->once()->andReturn($statement = m::mock(\PDOStatement::class)); $statement->shouldReceive('setFetchMode')->once()->with(\PDO::FETCH_ASSOC, null, [])->andReturnTrue(); $statement->shouldReceive('fetch')->with() diff --git a/tests/Dbal/Sqlite/UpdateFromTest.php b/tests/Dbal/Sqlite/UpdateFromTest.php new file mode 100644 index 0000000..838a9ab --- /dev/null +++ b/tests/Dbal/Sqlite/UpdateFromTest.php @@ -0,0 +1,34 @@ +dbal = new Sqlite($this->em); + } + + /** @test */ + public function executesAnUpdateFromStatement() + { + $this->pdo->shouldReceive('query')->with( + "UPDATE examples SET \"foo\" = names.foo FROM names WHERE exampleId = examples.id AND examples.id = 42" + )->once()->andReturn($statement = m::mock(PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); + + $this->em->query('examples') + ->join('names', 'exampleId = examples.id') + ->where('examples.id', 42) + ->update(['foo' => EM::raw('names.foo')]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index c0ddc5a..b47d6f9 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -43,10 +43,14 @@ protected function setUp() $this->pdo->shouldReceive('lastInsertId')->andReturn('666')->byDefault(); $this->em = $this->mocks['em'] = m::mock(TestEntityManager::class, [])->makePartial(); - $this->em->shouldReceive('getConnection')->andReturn($this->pdo)->byDefault(); + $this->em->shouldReceive('getConnection')->andReturnUsing(function () { + return $this->pdo; + })->byDefault(); $this->dbal = $this->mocks['dbal'] = m::mock(Dbal\Mysql::class, [$this->em])->makePartial(); - $this->em->shouldReceive('getDbal')->andReturn($this->dbal)->byDefault(); + $this->em->shouldReceive('getDbal')->andReturnUsing(function () { + return $this->dbal; + })->byDefault(); QueryBuilder::$defaultEntityManager = $this->em; } From 2635161e0251b7c3e34251d1983f68da0c84f66e Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Mon, 23 Nov 2020 08:28:33 +0100 Subject: [PATCH 07/16] extract insert statement and composite in expression --- src/Dbal/Dbal.php | 54 ++----------------- src/Dbal/Mysql.php | 6 +-- .../QueryLanguage/CompositeInExpression.php | 27 ++++++++++ .../CompositeInValuesExpression.php | 14 +++++ src/Dbal/QueryLanguage/InsertStatement.php | 34 ++++++++++++ src/QueryBuilder/QueryBuilder.php | 2 +- tests/QueryBuilder/WhereConditionsTest.php | 4 +- 7 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 src/Dbal/QueryLanguage/CompositeInExpression.php create mode 100644 src/Dbal/QueryLanguage/CompositeInValuesExpression.php create mode 100644 src/Dbal/QueryLanguage/InsertStatement.php diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index 7808902..99a1d3e 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -2,6 +2,8 @@ namespace ORM\Dbal; +use ORM\Dbal\QueryLanguage\CompositeInValuesExpression; +use ORM\Dbal\QueryLanguage\InsertStatement; use ORM\Dbal\QueryLanguage\UpdateStatement; use ORM\Entity; use ORM\EntityManager; @@ -19,12 +21,12 @@ abstract class Dbal { use Escaping; use UpdateStatement; + use InsertStatement; + use CompositeInValuesExpression; /** @var array */ protected static $typeMapping = []; - protected static $compositeWhereInTemplate = '(%s) %s (VALUES %s)'; - /** @var EntityManager */ protected $entityManager; @@ -257,33 +259,6 @@ protected static function assertSameType(array $entities) return true; } - /** - * Build an insert statement for $rows - * - * @param string $table - * @param array $rows - * @return string - */ - protected function buildInsert($table, array $rows) - { - // get all columns from rows - $columns = []; - foreach ($rows as $row) { - $columns = array_unique(array_merge($columns, array_keys($row))); - } - - $statement = 'INSERT INTO ' . $this->escapeIdentifier($table) . ' ' . - '(' . implode(',', array_map([$this, 'escapeIdentifier'], $columns)) . ') VALUES '; - - $statement .= implode(',', array_map(function ($values) use ($columns) { - return '(' . implode(',', array_map(function ($column) use ($values) { - return isset($values[$column]) ? $this->escapeValue($values[$column]) : $this->escapeNULL(); - }, $columns)) . ')'; - }, $rows)); - - return $statement; - } - /** * Update the autoincrement value * @@ -336,27 +311,6 @@ protected function syncInserted(Entity ...$entities) } } - /** - * Build a where in statement for composite keys - * - * @param array $cols - * @param array $keys - * @param bool $inverse Whether it should be a IN or NOT IN operator - * @return string - */ - public function buildCompositeWhereInStatement(array $cols, array $keys, $inverse = false) - { - $primaryKeys = array_map(function ($key) { - return '(' . implode(',', array_map([$this, 'escapeValue'], $key)) . ')'; - }, $keys); - - return vsprintf(static::$compositeWhereInTemplate, [ - implode(',', $cols), - $inverse ? 'NOT IN' : 'IN', - implode(',', $primaryKeys) - ]); - } - /** * Normalize $type * diff --git a/src/Dbal/Mysql.php b/src/Dbal/Mysql.php index 2ebeb36..4258242 100644 --- a/src/Dbal/Mysql.php +++ b/src/Dbal/Mysql.php @@ -2,6 +2,7 @@ namespace ORM\Dbal; +use ORM\Dbal\QueryLanguage\CompositeInExpression; use ORM\Dbal\QueryLanguage\UpdateJoinStatement; use ORM\Entity; use ORM\Exception; @@ -17,6 +18,7 @@ class Mysql extends Dbal { use UpdateJoinStatement; + use CompositeInExpression; protected static $typeMapping = [ 'tinyint' => Type\Number::class, @@ -46,8 +48,6 @@ class Mysql extends Dbal 'json' => Type\Json::class, ]; - protected static $compositeWhereInTemplate = '(%s) %s (%s)'; - public function insertAndSyncWithAutoInc(Entity ...$entities) { if (count($entities) === 0) { @@ -96,8 +96,6 @@ public function describe($table) public function update($table, array $where, array $updates, array $joins = []) { $query = $this->buildUpdateJoinStatement($table, $where, $updates, $joins); -// echo $query; -// return 0; $statement = $this->entityManager->getConnection()->query($query); return $statement->rowCount(); } diff --git a/src/Dbal/QueryLanguage/CompositeInExpression.php b/src/Dbal/QueryLanguage/CompositeInExpression.php new file mode 100644 index 0000000..3dd1a09 --- /dev/null +++ b/src/Dbal/QueryLanguage/CompositeInExpression.php @@ -0,0 +1,27 @@ +buildTuples($values)) . ')'; + } + + protected function buildTuples(array $values) + { + return array_map(function ($value) { + return '(' . implode(',', array_map([$this, 'escapeValue'], $value)) . ')'; + }, $values); + } +} diff --git a/src/Dbal/QueryLanguage/CompositeInValuesExpression.php b/src/Dbal/QueryLanguage/CompositeInValuesExpression.php new file mode 100644 index 0000000..46ead3b --- /dev/null +++ b/src/Dbal/QueryLanguage/CompositeInValuesExpression.php @@ -0,0 +1,14 @@ +buildTuples($values)) . ')'; + } +} diff --git a/src/Dbal/QueryLanguage/InsertStatement.php b/src/Dbal/QueryLanguage/InsertStatement.php new file mode 100644 index 0000000..681b297 --- /dev/null +++ b/src/Dbal/QueryLanguage/InsertStatement.php @@ -0,0 +1,34 @@ +escapeIdentifier($table) . ' ' . + '(' . implode(',', array_map([$this, 'escapeIdentifier'], $columns)) . ') VALUES '; + + $statement .= implode(',', array_map(function ($values) use ($columns) { + return '(' . implode(',', array_map(function ($column) use ($values) { + return isset($values[$column]) ? $this->escapeValue($values[$column]) : $this->escapeNULL(); + }, $columns)) . ')'; + }, $rows)); + + return $statement; + } + +} diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index 1ddbc54..0ad35f0 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -167,7 +167,7 @@ public function buildWhereInExpression($column, array $values, $inverse = false) return $inverse ? '1 = 1' : '1 = 0'; } elseif (is_array($column) && count($column) > 1) { return $em->getDbal() - ->buildCompositeWhereInStatement($column, $values, $inverse); + ->buildCompositeInExpression($column, $values, $inverse); } else { if (is_array($column)) { $column = $this->first($column); diff --git a/tests/QueryBuilder/WhereConditionsTest.php b/tests/QueryBuilder/WhereConditionsTest.php index 87096e9..bf694c3 100644 --- a/tests/QueryBuilder/WhereConditionsTest.php +++ b/tests/QueryBuilder/WhereConditionsTest.php @@ -246,7 +246,7 @@ public function compositeWhereInStatementUsesDbal() $cols = ['a', 'b']; $values = [[42, 23], [23, 42]]; - $this->dbal->shouldReceive('buildCompositeWhereInStatement') + $this->dbal->shouldReceive('buildCompositeInExpression') ->with($cols, $values, false)->once()->passthru(); $query->whereIn($cols, $values); @@ -261,7 +261,7 @@ public function compositeWhereNotInStatementUsesDbal() $cols = ['a', 'b']; $values = [[42, 23], [23, 42]]; - $this->dbal->shouldReceive('buildCompositeWhereInStatement') + $this->dbal->shouldReceive('buildCompositeInExpression') ->with($cols, $values, true)->once()->passthru(); $query->whereNotIn($cols, $values); From abcf36376721c3732b7a32c23c6d7a626fa8f283 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Tue, 24 Nov 2020 08:26:43 +0100 Subject: [PATCH 08/16] fix coding style and update codesniffer --- composer.json | 2 +- src/Dbal/QueryLanguage/InsertStatement.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8fd5ff3..bebd178 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "require-dev": { "mockery/mockery": "^1.1", "phpunit/phpunit": "^5.6", - "squizlabs/php_codesniffer": "^2.7" + "squizlabs/php_codesniffer": "^3.5.8" }, "suggest": { "mockery/mockery": "^1.1" diff --git a/src/Dbal/QueryLanguage/InsertStatement.php b/src/Dbal/QueryLanguage/InsertStatement.php index 681b297..74e2bc7 100644 --- a/src/Dbal/QueryLanguage/InsertStatement.php +++ b/src/Dbal/QueryLanguage/InsertStatement.php @@ -30,5 +30,4 @@ protected function buildInsert($table, array $rows) return $statement; } - } From 2f1f3525d60e4ef7d401dea4f53d744cfc8fdb61 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Tue, 24 Nov 2020 08:58:12 +0100 Subject: [PATCH 09/16] extract delete statement and implement public interface --- src/Dbal/Dbal.php | 40 ++++++++++++++++---- src/Dbal/QueryLanguage/DeleteStatement.php | 14 +++++++ src/Dbal/QueryLanguage/UpdateStatement.php | 17 +-------- src/Dbal/QueryLanguage/WhereClause.php | 21 ++++++++++ src/EntityManager.php | 2 +- src/QueryBuilder/ExecutesQueries.php | 13 +++++++ tests/EntityManager/DataModificationTest.php | 8 +++- tests/Observer/EventTest.php | 2 +- tests/QueryBuilder/BasicTest.php | 15 ++++++++ 9 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 src/Dbal/QueryLanguage/DeleteStatement.php create mode 100644 src/Dbal/QueryLanguage/WhereClause.php diff --git a/src/Dbal/Dbal.php b/src/Dbal/Dbal.php index 99a1d3e..d49243f 100644 --- a/src/Dbal/Dbal.php +++ b/src/Dbal/Dbal.php @@ -3,6 +3,7 @@ namespace ORM\Dbal; use ORM\Dbal\QueryLanguage\CompositeInValuesExpression; +use ORM\Dbal\QueryLanguage\DeleteStatement; use ORM\Dbal\QueryLanguage\InsertStatement; use ORM\Dbal\QueryLanguage\UpdateStatement; use ORM\Entity; @@ -23,6 +24,9 @@ abstract class Dbal use UpdateStatement; use InsertStatement; use CompositeInValuesExpression; + use DeleteStatement { + UpdateStatement::buildWhereClause insteadof DeleteStatement; + } /** @var array */ protected static $typeMapping = []; @@ -214,6 +218,29 @@ public function updateEntity(Entity $entity) return $this->entityManager->sync($entity, true); } + /** + * Delete rows from $table using $where conditions + * + * Where conditions can be an array of key => value pairs to check for equality or an array of expressions. + * + * Examples: + * `$dbal->delete('someTable', ['id' => 23])` + * `$dbal->delete('user', ['name = \'john\'', 'OR email=\'john.doe@example.com\''])` + * + * Tip: Use the query builder to construct where conditions: + * `$em->query('user')->where('name', 'john')->orWhere('email', '...')->delete();` + * + * @param string $table The table where to delete rows + * @param array $where An array of where conditions + * @return int The number of deleted rows + */ + public function delete($table, array $where) + { + $query = $this->buildDeleteStatement($table, $where); + $statement = $this->entityManager->getConnection()->query($query); + return $statement->rowCount(); + } + /** * Delete $entity from database * @@ -222,19 +249,16 @@ public function updateEntity(Entity $entity) * @param Entity $entity * @return bool */ - public function delete(Entity $entity) + public function deleteEntity(Entity $entity) { $primaryKey = $entity->getPrimaryKey(); - $where = []; + $where = []; foreach ($primaryKey as $attribute => $value) { - $col = $entity::getColumnName($attribute); - $where[] = $this->escapeIdentifier($col) . ' = ' . $this->escapeValue($value); + $col = $entity::getColumnName($attribute); + $where[$col] = $value; } - $statement = 'DELETE FROM ' . $this->escapeIdentifier($entity::getTableName()) . ' ' . - 'WHERE ' . implode(' AND ', $where); - $this->entityManager->getConnection()->query($statement); - + $this->delete($entity::getTableName(), $where); return true; } diff --git a/src/Dbal/QueryLanguage/DeleteStatement.php b/src/Dbal/QueryLanguage/DeleteStatement.php new file mode 100644 index 0000000..7c86ab5 --- /dev/null +++ b/src/Dbal/QueryLanguage/DeleteStatement.php @@ -0,0 +1,14 @@ +escapeIdentifier($table) . + $this->buildWhereClause($where); + } +} diff --git a/src/Dbal/QueryLanguage/UpdateStatement.php b/src/Dbal/QueryLanguage/UpdateStatement.php index 61139a0..e2f4d96 100644 --- a/src/Dbal/QueryLanguage/UpdateStatement.php +++ b/src/Dbal/QueryLanguage/UpdateStatement.php @@ -4,6 +4,8 @@ trait UpdateStatement { + use WhereClause; + protected function buildUpdateStatement($table, array $where, array $updates) { return 'UPDATE ' . $this->escapeIdentifier($table) . @@ -17,19 +19,4 @@ protected function buildSetClause(array $updates) return $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($value); }, array_keys($updates), $updates)); } - - protected function buildWhereClause(array $where) - { - $whereClause = !empty($where) ? ' WHERE ' : ''; - $i = 0; - foreach ($where as $column => $condition) { - if ($i > 0 && (!is_numeric($column) || !preg_match('/^\s*(AND|OR)/i', $condition))) { - $whereClause .= ' AND '; - } - $whereClause .= !is_numeric($column) ? - $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($condition) : $condition; - $i++; - } - return $whereClause; - } } diff --git a/src/Dbal/QueryLanguage/WhereClause.php b/src/Dbal/QueryLanguage/WhereClause.php new file mode 100644 index 0000000..0242201 --- /dev/null +++ b/src/Dbal/QueryLanguage/WhereClause.php @@ -0,0 +1,21 @@ + $condition) { + if ($i > 0 && (!is_numeric($column) || !preg_match('/^\s*(AND|OR)/i', $condition))) { + $whereClause .= ' AND '; + } + $whereClause .= !is_numeric($column) ? + $this->escapeIdentifier($column) . ' = ' . $this->escapeValue($condition) : $condition; + $i++; + } + return $whereClause; + } +} diff --git a/src/EntityManager.php b/src/EntityManager.php index e73a96f..e8f9cb6 100644 --- a/src/EntityManager.php +++ b/src/EntityManager.php @@ -496,7 +496,7 @@ public function delete(Entity $entity) if ($this->fire(new Deleting($entity)) === false) { return false; } - $this->getDbal()->delete($entity); + $this->getDbal()->deleteEntity($entity); $entity->setOriginalData([]); $this->fire(new Deleted($entity)); return true; diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php index 19fd2a1..d85bb25 100644 --- a/src/QueryBuilder/ExecutesQueries.php +++ b/src/QueryBuilder/ExecutesQueries.php @@ -127,6 +127,19 @@ public function update(array $updates) ); } + /** + * Execute a delete statement for the current query + * + * @return int The number of deleted rows + */ + public function delete() + { + return $this->entityManager->getDbal()->delete( + EntityManager::raw($this->tableName . ($this->alias ? ' AS ' . $this->alias : '')), + $this->where + ); + } + /** * Query database and return result * diff --git a/tests/EntityManager/DataModificationTest.php b/tests/EntityManager/DataModificationTest.php index 4112250..9108ea3 100644 --- a/tests/EntityManager/DataModificationTest.php +++ b/tests/EntityManager/DataModificationTest.php @@ -408,7 +408,9 @@ public function deleteStatement($entity, $statement) * @test */ public function deleteReturnsSuccess($entity, $statement) { - $this->pdo->shouldReceive('query')->with($statement)->once()->andReturn(m::mock(\PDOStatement::class)); + $this->pdo->shouldReceive('query')->with($statement)->once() + ->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); $result = $this->em->delete($entity); @@ -422,7 +424,9 @@ public function deleteRemovesOriginalData(Entity $entity, $statement) $entity->setOriginalData($entity->getData()); self::assertFalse($entity->isDirty()); - $this->pdo->shouldReceive('query')->with($statement)->once()->andReturn(m::mock(\PDOStatement::class)); + $this->pdo->shouldReceive('query')->with($statement)->once() + ->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->andReturn(1); $this->em->delete($entity); diff --git a/tests/Observer/EventTest.php b/tests/Observer/EventTest.php index 00441b5..caa1276 100644 --- a/tests/Observer/EventTest.php +++ b/tests/Observer/EventTest.php @@ -149,7 +149,7 @@ public function deletedAfterDelete() $this->em->shouldReceive('fire')->withArgs(function (Event $event) { return $event instanceof Event\Deleting && $event->entity instanceof Article; })->once()->andReturnTrue(); - $this->mocks['dbal']->shouldReceive('delete')->with($article)->once()->andReturnTrue(); + $this->mocks['dbal']->shouldReceive('deleteEntity')->with($article)->once()->andReturnTrue(); $this->em->shouldReceive('fire')->withArgs(function (Event $event) { return $event instanceof Event\Deleted && $event->entity instanceof Article; })->once()->andReturnTrue(); diff --git a/tests/QueryBuilder/BasicTest.php b/tests/QueryBuilder/BasicTest.php index f011288..1679b1b 100644 --- a/tests/QueryBuilder/BasicTest.php +++ b/tests/QueryBuilder/BasicTest.php @@ -224,4 +224,19 @@ public function executesDbalUpdate() $query->update(['col1' => 'value']); } + + /** @test */ + public function executesDbalDelete() + { + $query = new QueryBuilder('foo', '', $this->em); + $query->where('foo.id', 42); + $query->join('bar', 'foo.barId = bar.id'); + + $this->dbal->shouldReceive('delete')->with( + m::type(Expression::class), + ['foo.id = 42'] + )->once()->andReturn(1); + + $query->delete(); + } } From b9d775320c3b0ea2e843a5f2cb874e57175fd68a Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Thu, 26 Nov 2020 08:43:38 +0100 Subject: [PATCH 10/16] update api reference --- docs/reference.md | 1363 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 1222 insertions(+), 141 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index a30e464..31f5aff 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -25,6 +25,7 @@ permalink: /reference.html * [Column](#ormdbalcolumn) * [Dbal](#ormdbaldbal) * [Error](#ormdbalerror) +* [Expression](#ormdbalexpression) * [Mysql](#ormdbalmysql) * [Other](#ormdbalother) * [Pgsql](#ormdbalpgsql) @@ -1225,7 +1226,6 @@ public function validate( $value ): boolean|ORM\Dbal\Error |------------|------|------|---------------------------------------| | **protected** | `$booleanFalse` | **string** | | | **protected** | `$booleanTrue` | **string** | | -| **protected static** | `$compositeWhereInTemplate` | | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | | | **protected** | `$identifierDivider` | **string** | | | **protected** | `$quotingCharacter` | **string** | | @@ -1237,9 +1237,13 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [__construct](#ormdbaldbal__construct) Dbal constructor. * [assertSameType](#ormdbaldbalassertsametype) -* [buildCompositeWhereInStatement](#ormdbaldbalbuildcompositewhereinstatement) Build a where in statement for composite keys -* [buildInsertStatement](#ormdbaldbalbuildinsertstatement) Build the insert statement for $entity -* [delete](#ormdbaldbaldelete) Delete $entity from database +* [buildCompositeInExpression](#ormdbaldbalbuildcompositeinexpression) +* [buildDeleteStatement](#ormdbaldbalbuilddeletestatement) +* [buildInsert](#ormdbaldbalbuildinsert) Build an insert statement for $rows +* [buildSetClause](#ormdbaldbalbuildsetclause) +* [buildUpdateStatement](#ormdbaldbalbuildupdatestatement) +* [delete](#ormdbaldbaldelete) Delete rows from $table using $where conditions +* [deleteEntity](#ormdbaldbaldeleteentity) Delete $entity from database * [describe](#ormdbaldbaldescribe) Describe a table * [escapeBoolean](#ormdbaldbalescapeboolean) Escape a boolean for query * [escapeDateTime](#ormdbaldbalescapedatetime) Escape a date time object for query @@ -1250,12 +1254,14 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [escapeString](#ormdbaldbalescapestring) Escape a string for query * [escapeValue](#ormdbaldbalescapevalue) Returns $value formatted to use in a sql statement. * [extractParenthesis](#ormdbaldbalextractparenthesis) Extract content from parenthesis in $type -* [insert](#ormdbaldbalinsert) Insert $entities into database +* [insert](#ormdbaldbalinsert) * [insertAndSync](#ormdbaldbalinsertandsync) Insert $entities and update with default values from database * [insertAndSyncWithAutoInc](#ormdbaldbalinsertandsyncwithautoinc) Insert $entities and sync with auto increment primary key +* [insertEntities](#ormdbaldbalinsertentities) Insert $entities into database * [normalizeType](#ormdbaldbalnormalizetype) Normalize $type * [setOption](#ormdbaldbalsetoption) Set $option to $value * [syncInserted](#ormdbaldbalsyncinserted) Sync the $entities after insert +* [update](#ormdbaldbalupdate) Update $table using $where to set $updates * [updateAutoincrement](#ormdbaldbalupdateautoincrement) Update the autoincrement value #### ORM\Dbal\Dbal::__construct @@ -1308,42 +1314,60 @@ protected static function assertSameType( -#### ORM\Dbal\Dbal::buildCompositeWhereInStatement +#### ORM\Dbal\Dbal::buildCompositeInExpression ```php -public function buildCompositeWhereInStatement( - array $cols, array $keys, boolean $inverse = false -): string +public function buildCompositeInExpression( + array $cols, array $values, $inverse = false +) ``` -##### Build a where in statement for composite keys **Visibility:** this method is **public**. -
- **Returns**: this method returns **string**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `$cols` | **array** | | -| `$keys` | **array** | | -| `$inverse` | **boolean** | Whether it should be a IN or NOT IN operator | +| `$values` | **array** | | +| `$inverse` | | | -#### ORM\Dbal\Dbal::buildInsertStatement +#### ORM\Dbal\Dbal::buildDeleteStatement ```php -protected function buildInsertStatement( - ORM\Entity $entity, array<\ORM\Entity> $entities -): string +protected function buildDeleteStatement( $table, array $where ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | + + + +#### ORM\Dbal\Dbal::buildInsert + +```php +protected function buildInsert( string $table, array $rows ): string ``` -##### Build the insert statement for $entity +##### Build an insert statement for $rows @@ -1356,15 +1380,91 @@ protected function buildInsertStatement( | Parameter | Type | Description | |-----------|------|-------------| -| `$entity` | **\ORM\Entity** | | -| `$entities` | **array<\ORM\Entity>** | | +| `$table` | **string** | | +| `$rows` | **array** | | + + + +#### ORM\Dbal\Dbal::buildSetClause + +```php +protected function buildSetClause( array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | | + + + +#### ORM\Dbal\Dbal::buildUpdateStatement + +```php +protected function buildUpdateStatement( $table, array $where, array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | #### ORM\Dbal\Dbal::delete ```php -public function delete( ORM\Entity $entity ): boolean +public function delete( string $table, array $where ): integer +``` + +##### Delete rows from $table using $where conditions + +Where conditions can be an array of key => value pairs to check for equality or an array of expressions. + +Examples: +`$dbal->delete('someTable', ['id' => 23])` +`$dbal->delete('user', ['name = \'john\'', 'OR email=\'john.doe@example.com\''])` + +Tip: Use the query builder to construct where conditions: +`$em->query('user')->where('name', 'john')->orWhere('email', '...')->delete();` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table where to delete rows | +| `$where` | **array** | An array of where conditions | + + + +#### ORM\Dbal\Dbal::deleteEntity + +```php +public function deleteEntity( ORM\Entity $entity ): boolean ``` ##### Delete $entity from database @@ -1613,23 +1713,22 @@ protected function extractParenthesis( string $type ): string #### ORM\Dbal\Dbal::insert ```php -public function insert( ORM\Entity $entities ): boolean +public function insert( $table, array $rows ) ``` -##### Insert $entities into database -The entities have to be from same type otherwise a InvalidArgument will be thrown. + **Visibility:** this method is **public**.
- **Returns**: this method returns **boolean** -
**Throws:** this method may throw **\ORM\Exception\InvalidArgument**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$entities` | **\ORM\Entity** | | +| `$table` | | | +| `$rows` | **array** | | @@ -1681,6 +1780,29 @@ The entities have to be from same type otherwise a InvalidArgument will be throw +#### ORM\Dbal\Dbal::insertEntities + +```php +public function insertEntities( ORM\Entity $entities ): boolean +``` + +##### Insert $entities into database + +The entities have to be from same type otherwise a InvalidArgument will be thrown. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **boolean** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$entities` | **\ORM\Entity** | | + + + #### ORM\Dbal\Dbal::normalizeType ```php @@ -1750,6 +1872,43 @@ protected function syncInserted( ORM\Entity $entities ) +#### ORM\Dbal\Dbal::update + +```php +public function update( + string $table, array $where, array $updates, array $joins = array() +): integer +``` + +##### Update $table using $where to set $updates + +Simple usage: `update('table', ['id' => 23], ['name' => 'John Doe'])` + +For advanced queries with parenthesis, joins (if supported from your DBMS) etc. use QueryBuilder: + +```php +$em->query('table') + ->where('birth_date', '>', EM::raw('DATE_SUB(NOW(), INTERVAL 18 YEARS)')) + ->update(['teenager' => true]); +``` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
**Throws:** this method may throw **\ORM\Exception\UnsupportedDriver**
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table to update | +| `$where` | **array** | An array of where conditions | +| `$updates` | **array** | An array of columns to update | +| `$joins` | **array** | For internal use from query builder only | + + + #### ORM\Dbal\Dbal::updateAutoincrement ```php @@ -3369,6 +3528,7 @@ Supported: * [convertPlaceholders](#ormentityfetcherconvertplaceholders) Replaces questionmarks in $expression with $args * [count](#ormentityfetchercount) Get the count of the resulting items * [createRelatedJoin](#ormentityfetchercreaterelatedjoin) Create the join with $join type +* [delete](#ormentityfetcherdelete) Execute a delete statement for the current query * [first](#ormentityfetcherfirst) Get the first item of an array * [fullJoin](#ormentityfetcherfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormentityfetchergetdefaultoperator) Get the default operator for $value @@ -3398,6 +3558,7 @@ Supported: * [setQuery](#ormentityfetchersetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormentityfetchertoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormentityfetchertranslatecolumn) Translate attribute names in an expression to their column names +* [update](#ormentityfetcherupdate) Execute an update statement for the current query * [where](#ormentityfetcherwhere) Alias for andWhere * [whereIn](#ormentityfetcherwherein) Add a where in condition with AND. * [whereNotIn](#ormentityfetcherwherenotin) Add a where not in condition with AND. @@ -3645,6 +3806,24 @@ public function createRelatedJoin( $join, $relation ): $this +#### ORM\EntityFetcher::delete + +```php +public function delete(): integer +``` + +##### Execute a delete statement for the current query + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ + + #### ORM\EntityFetcher::first ```php @@ -4346,6 +4525,33 @@ protected function translateColumn( string $expression ): string +#### ORM\EntityFetcher::update + +```php +public function update( array $updates ): integer +``` + +##### Execute an update statement for the current query + +**NOTE:** not all drivers support UPDATE with JOIN (or FROM). Has to be implemented in the database abstraction +layer. + +$updates should be an array which columns to update with what value. Use expressions to bypass escaping. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | An array of columns to update | + + + #### ORM\EntityFetcher::where ```php @@ -4536,6 +4742,7 @@ Supported: * [convertPlaceholders](#ormtestingentityfetchermockconvertplaceholders) Replaces question marks in $expression with $args * [count](#ormtestingentityfetchermockcount) Get the count of the resulting items * [createRelatedJoin](#ormtestingentityfetchermockcreaterelatedjoin) Create the join with $join type +* [delete](#ormtestingentityfetchermockdelete) Execute a delete statement for the current query * [first](#ormtestingentityfetchermockfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormtestingentityfetchermockgetdefaultoperator) Get the default operator for $value @@ -4565,6 +4772,7 @@ Supported: * [setQuery](#ormtestingentityfetchermocksetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormtestingentityfetchermocktoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormtestingentityfetchermocktranslatecolumn) Translate attribute names in an expression to their column names +* [update](#ormtestingentityfetchermockupdate) Execute an update statement for the current query * [where](#ormtestingentityfetchermockwhere) Alias for andWhere * [whereIn](#ormtestingentityfetchermockwherein) Add a where in condition with AND. * [whereNotIn](#ormtestingentityfetchermockwherenotin) Add a where not in condition with AND. @@ -4805,6 +5013,24 @@ public function createRelatedJoin( $join, $relation ): $this +#### ORM\Testing\EntityFetcherMock::delete + +```php +public function delete(): integer +``` + +##### Execute a delete statement for the current query + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ + + #### ORM\Testing\EntityFetcherMock::first ```php @@ -5506,6 +5732,33 @@ protected function translateColumn( string $expression ): string +#### ORM\Testing\EntityFetcherMock::update + +```php +public function update( array $updates ): integer +``` + +##### Execute an update statement for the current query + +**NOTE:** not all drivers support UPDATE with JOIN (or FROM). Has to be implemented in the database abstraction +layer. + +$updates should be an array which columns to update with what value. Use expressions to bypass escaping. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | An array of columns to update | + + + #### ORM\Testing\EntityFetcherMock::where ```php @@ -5699,7 +5952,7 @@ private function wherePrefix( string $bool ): string * [finishBulkInserts](#ormentitymanagerfinishbulkinserts) Finish the bulk insert for $class. * [fire](#ormentitymanagerfire) Fire $event on $entity * [getConnection](#ormentitymanagergetconnection) Get the pdo connection. -* [getDbal](#ormentitymanagergetdbal) Get the Datbase Abstraction Layer +* [getDbal](#ormentitymanagergetdbal) Get the Database Abstraction Layer * [getInstance](#ormentitymanagergetinstance) Get an instance of the EntityManager. * [getInstanceByNameSpace](#ormentitymanagergetinstancebynamespace) Get the instance by NameSpace mapping * [getInstanceByParent](#ormentitymanagergetinstancebyparent) Get the instance by Parent class mapping @@ -5709,6 +5962,7 @@ private function wherePrefix( string $bool ): string * [map](#ormentitymanagermap) Map $entity in the entity map * [observe](#ormentitymanagerobserve) Observe $class using $observer * [query](#ormentitymanagerquery) Get a query builder for $table +* [raw](#ormentitymanagerraw) Create a raw expression from $expression to disable escaping * [setConnection](#ormentitymanagersetconnection) Add connection after instantiation * [setOption](#ormentitymanagersetoption) Set $option to $value * [setResolver](#ormentitymanagersetresolver) Overwrite the functionality of ::getInstance($class) by $resolver($class) @@ -6052,7 +6306,7 @@ public function getConnection(): PDO public function getDbal(): ORM\Dbal\Dbal ``` -##### Get the Datbase Abstraction Layer +##### Get the Database Abstraction Layer @@ -6293,6 +6547,30 @@ public function query( +#### ORM\EntityManager::raw + +```php +public static function raw( string $expression ): ORM\Dbal\Expression +``` + +##### Create a raw expression from $expression to disable escaping + + + +**Static:** this method is **static**. +
**Visibility:** this method is **public**. +
+ **Returns**: this method returns **\ORM\Dbal\Expression** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$expression` | **string** | | + + + #### ORM\EntityManager::setConnection ```php @@ -6467,7 +6745,7 @@ At the end you should call finish bulk insert otherwise you may loose data. * [finishBulkInserts](#ormtestingentitymanagermockfinishbulkinserts) Finish the bulk insert for $class. * [fire](#ormtestingentitymanagermockfire) Fire $event on $entity * [getConnection](#ormtestingentitymanagermockgetconnection) Get the pdo connection. -* [getDbal](#ormtestingentitymanagermockgetdbal) Get the Datbase Abstraction Layer +* [getDbal](#ormtestingentitymanagermockgetdbal) Get the Database Abstraction Layer * [getInstance](#ormtestingentitymanagermockgetinstance) Get an instance of the EntityManager. * [getInstanceByNameSpace](#ormtestingentitymanagermockgetinstancebynamespace) Get the instance by NameSpace mapping * [getInstanceByParent](#ormtestingentitymanagermockgetinstancebyparent) Get the instance by Parent class mapping @@ -6478,6 +6756,7 @@ At the end you should call finish bulk insert otherwise you may loose data. * [map](#ormtestingentitymanagermockmap) Map $entity in the entity map * [observe](#ormtestingentitymanagermockobserve) Observe $class using $observer * [query](#ormtestingentitymanagermockquery) Get a query builder for $table +* [raw](#ormtestingentitymanagermockraw) Create a raw expression from $expression to disable escaping * [retrieve](#ormtestingentitymanagermockretrieve) Retrieve an entity by $primaryKey * [setConnection](#ormtestingentitymanagermocksetconnection) Add connection after instantiation * [setOption](#ormtestingentitymanagermocksetoption) Set $option to $value @@ -6873,7 +7152,7 @@ public function getConnection(): PDO public function getDbal(): ORM\Dbal\Dbal ``` -##### Get the Datbase Abstraction Layer +##### Get the Database Abstraction Layer @@ -7138,6 +7417,30 @@ public function query( +#### ORM\Testing\EntityManagerMock::raw + +```php +public static function raw( string $expression ): ORM\Dbal\Expression +``` + +##### Create a raw expression from $expression to disable escaping + + + +**Static:** this method is **static**. +
**Visibility:** this method is **public**. +
+ **Returns**: this method returns **\ORM\Dbal\Expression** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$expression` | **string** | | + + + #### ORM\Testing\EntityManagerMock::retrieve ```php @@ -7605,41 +7908,105 @@ Every ORM exception extends this class. So you can easily catch all exceptions f --- -### ORM\Event\Fetched - -**Extends:** [ORM\Event](#ormevent) - +### ORM\Dbal\Expression -#### Constants -| Name | Value | -|------|-------| -| NAME | `'fetched'` | #### Properties | Visibility | Name | Type | Description | |------------|------|------|---------------------------------------| -| **protected** | `$data` | **array** | | -| **protected** | `$entity` | ** \ ORM \ Entity** | | -| **protected** | `$rawData` | **array** | | -| **protected** | `$stopped` | **boolean** | | +| **protected** | `$expression` | | | #### Methods -* [__construct](#ormeventfetched__construct) -* [__get](#ormeventfetched__get) -* [stop](#ormeventfetchedstop) +* [__construct](#ormdbalexpression__construct) Expression constructor. +* [__toString](#ormdbalexpression__tostring) -#### ORM\Event\Fetched::__construct +#### ORM\Dbal\Expression::__construct + +```php +public function __construct( string $expression ) +``` + +##### Expression constructor. + + + +**Visibility:** this method is **public**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$expression` | **string** | | + + + +#### ORM\Dbal\Expression::__toString + +```php +public function __toString() +``` + + + + +**Visibility:** this method is **public**. +
+ + + + + + +--- + +### ORM\Event\Fetched + +**Extends:** [ORM\Event](#ormevent) + + + + + + + +#### Constants + +| Name | Value | +|------|-------| +| NAME | `'fetched'` | + + +#### Properties + +| Visibility | Name | Type | Description | +|------------|------|------|---------------------------------------| +| **protected** | `$data` | **array** | | +| **protected** | `$entity` | ** \ ORM \ Entity** | | +| **protected** | `$rawData` | **array** | | +| **protected** | `$stopped` | **boolean** | | + + + +#### Methods + +* [__construct](#ormeventfetched__construct) +* [__get](#ormeventfetched__get) +* [stop](#ormeventfetchedstop) + +#### ORM\Event\Fetched::__construct ```php public function __construct( ORM\Entity $entity, array $rawData ) @@ -8371,7 +8738,6 @@ public function setRelated( ORM\Entity $self, $entity = null ) |------------|------|------|---------------------------------------| | **protected** | `$booleanFalse` | **string** | | | **protected** | `$booleanTrue` | **string** | | -| **protected static** | `$compositeWhereInTemplate` | | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | | | **protected** | `$identifierDivider` | **string** | | | **protected** | `$quotingCharacter` | **string** | | @@ -8383,9 +8749,15 @@ public function setRelated( ORM\Entity $self, $entity = null ) * [__construct](#ormdbalmysql__construct) Dbal constructor. * [assertSameType](#ormdbalmysqlassertsametype) -* [buildCompositeWhereInStatement](#ormdbalmysqlbuildcompositewhereinstatement) Build a where in statement for composite keys -* [buildInsertStatement](#ormdbalmysqlbuildinsertstatement) Build the insert statement for $entity -* [delete](#ormdbalmysqldelete) Delete $entity from database +* [buildCompositeInExpression](#ormdbalmysqlbuildcompositeinexpression) +* [buildDeleteStatement](#ormdbalmysqlbuilddeletestatement) +* [buildInsert](#ormdbalmysqlbuildinsert) Build an insert statement for $rows +* [buildSetClause](#ormdbalmysqlbuildsetclause) +* [buildTuples](#ormdbalmysqlbuildtuples) +* [buildUpdateJoinStatement](#ormdbalmysqlbuildupdatejoinstatement) +* [buildUpdateStatement](#ormdbalmysqlbuildupdatestatement) +* [delete](#ormdbalmysqldelete) Delete rows from $table using $where conditions +* [deleteEntity](#ormdbalmysqldeleteentity) Delete $entity from database * [describe](#ormdbalmysqldescribe) Describe a table * [escapeBoolean](#ormdbalmysqlescapeboolean) Escape a boolean for query * [escapeDateTime](#ormdbalmysqlescapedatetime) Escape a date time object for query @@ -8396,13 +8768,15 @@ public function setRelated( ORM\Entity $self, $entity = null ) * [escapeString](#ormdbalmysqlescapestring) Escape a string for query * [escapeValue](#ormdbalmysqlescapevalue) Returns $value formatted to use in a sql statement. * [extractParenthesis](#ormdbalmysqlextractparenthesis) Extract content from parenthesis in $type -* [insert](#ormdbalmysqlinsert) Insert $entities into database +* [insert](#ormdbalmysqlinsert) * [insertAndSync](#ormdbalmysqlinsertandsync) Insert $entities and update with default values from database * [insertAndSyncWithAutoInc](#ormdbalmysqlinsertandsyncwithautoinc) Insert $entities and sync with auto increment primary key +* [insertEntities](#ormdbalmysqlinsertentities) Insert $entities into database * [normalizeColumnDefinition](#ormdbalmysqlnormalizecolumndefinition) Normalize a column definition * [normalizeType](#ormdbalmysqlnormalizetype) Normalize $type * [setOption](#ormdbalmysqlsetoption) Set $option to $value * [syncInserted](#ormdbalmysqlsyncinserted) Sync the $entities after insert +* [update](#ormdbalmysqlupdate) Update $table using $where to set $updates * [updateAutoincrement](#ormdbalmysqlupdateautoincrement) Update the autoincrement value #### ORM\Dbal\Mysql::__construct @@ -8455,42 +8829,60 @@ protected static function assertSameType( -#### ORM\Dbal\Mysql::buildCompositeWhereInStatement +#### ORM\Dbal\Mysql::buildCompositeInExpression ```php -public function buildCompositeWhereInStatement( - array $cols, array $keys, boolean $inverse = false -): string +public function buildCompositeInExpression( + array $cols, array $values, $inverse = false +) ``` -##### Build a where in statement for composite keys **Visibility:** this method is **public**. -
- **Returns**: this method returns **string**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `$cols` | **array** | | -| `$keys` | **array** | | -| `$inverse` | **boolean** | Whether it should be a IN or NOT IN operator | +| `$values` | **array** | | +| `$inverse` | | | -#### ORM\Dbal\Mysql::buildInsertStatement +#### ORM\Dbal\Mysql::buildDeleteStatement ```php -protected function buildInsertStatement( - ORM\Entity $entity, array<\ORM\Entity> $entities -): string +protected function buildDeleteStatement( $table, array $where ) ``` -##### Build the insert statement for $entity + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | + + + +#### ORM\Dbal\Mysql::buildInsert + +```php +protected function buildInsert( string $table, array $rows ): string +``` + +##### Build an insert statement for $rows @@ -8503,15 +8895,138 @@ protected function buildInsertStatement( | Parameter | Type | Description | |-----------|------|-------------| -| `$entity` | **\ORM\Entity** | | -| `$entities` | **array<\ORM\Entity>** | | +| `$table` | **string** | | +| `$rows` | **array** | | + + + +#### ORM\Dbal\Mysql::buildSetClause + +```php +protected function buildSetClause( array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | | + + + +#### ORM\Dbal\Mysql::buildTuples + +```php +protected function buildTuples( array $values ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$values` | **array** | | + + + +#### ORM\Dbal\Mysql::buildUpdateJoinStatement + +```php +protected function buildUpdateJoinStatement( + $table, array $where, array $updates, array $joins +) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | +| `$joins` | **array** | | + + + +#### ORM\Dbal\Mysql::buildUpdateStatement + +```php +protected function buildUpdateStatement( $table, array $where, array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | #### ORM\Dbal\Mysql::delete ```php -public function delete( ORM\Entity $entity ): boolean +public function delete( string $table, array $where ): integer +``` + +##### Delete rows from $table using $where conditions + +Where conditions can be an array of key => value pairs to check for equality or an array of expressions. + +Examples: +`$dbal->delete('someTable', ['id' => 23])` +`$dbal->delete('user', ['name = \'john\'', 'OR email=\'john.doe@example.com\''])` + +Tip: Use the query builder to construct where conditions: +`$em->query('user')->where('name', 'john')->orWhere('email', '...')->delete();` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table where to delete rows | +| `$where` | **array** | An array of where conditions | + + + +#### ORM\Dbal\Mysql::deleteEntity + +```php +public function deleteEntity( ORM\Entity $entity ): boolean ``` ##### Delete $entity from database @@ -8760,23 +9275,22 @@ protected function extractParenthesis( string $type ): string #### ORM\Dbal\Mysql::insert ```php -public function insert( ORM\Entity $entities ): boolean +public function insert( $table, array $rows ) ``` -##### Insert $entities into database -The entities have to be from same type otherwise a InvalidArgument will be thrown. + **Visibility:** this method is **public**.
- **Returns**: this method returns **boolean** -
**Throws:** this method may throw **\ORM\Exception\InvalidArgument**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$entities` | **\ORM\Entity** | | +| `$table` | | | +| `$rows` | **array** | | @@ -8828,6 +9342,29 @@ The entities have to be from same type otherwise a InvalidArgument will be throw +#### ORM\Dbal\Mysql::insertEntities + +```php +public function insertEntities( ORM\Entity $entities ): boolean +``` + +##### Insert $entities into database + +The entities have to be from same type otherwise a InvalidArgument will be thrown. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **boolean** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$entities` | **\ORM\Entity** | | + + + #### ORM\Dbal\Mysql::normalizeColumnDefinition ```php @@ -8921,6 +9458,43 @@ protected function syncInserted( ORM\Entity $entities ) +#### ORM\Dbal\Mysql::update + +```php +public function update( + string $table, array $where, array $updates, array $joins = array() +): integer +``` + +##### Update $table using $where to set $updates + +Simple usage: `update('table', ['id' => 23], ['name' => 'John Doe'])` + +For advanced queries with parenthesis, joins (if supported from your DBMS) etc. use QueryBuilder: + +```php +$em->query('table') + ->where('birth_date', '>', EM::raw('DATE_SUB(NOW(), INTERVAL 18 YEARS)')) + ->update(['teenager' => true]); +``` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table to update | +| `$where` | **array** | An array of where conditions | +| `$updates` | **array** | An array of columns to update | +| `$joins` | **array** | For internal use from query builder only | + + + #### ORM\Dbal\Mysql::updateAutoincrement ```php @@ -10498,7 +11072,6 @@ public function setRelated( ORM\Entity $self, $entity = null ) |------------|------|------|---------------------------------------| | **protected** | `$booleanFalse` | **string** | | | **protected** | `$booleanTrue` | **string** | | -| **protected static** | `$compositeWhereInTemplate` | | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | | | **protected** | `$identifierDivider` | **string** | | | **protected** | `$quotingCharacter` | **string** | | @@ -11649,7 +12222,6 @@ If $values is empty the expression will be `1 = 1` because an empty parenthesis |------------|------|------|---------------------------------------| | **protected** | `$booleanFalse` | | | | **protected** | `$booleanTrue` | | | -| **protected static** | `$compositeWhereInTemplate` | | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | | | **protected** | `$identifierDivider` | **string** | | | **protected** | `$quotingCharacter` | **string** | | @@ -11661,9 +12233,15 @@ If $values is empty the expression will be `1 = 1` because an empty parenthesis * [__construct](#ormdbalpgsql__construct) Dbal constructor. * [assertSameType](#ormdbalpgsqlassertsametype) -* [buildCompositeWhereInStatement](#ormdbalpgsqlbuildcompositewhereinstatement) Build a where in statement for composite keys -* [buildInsertStatement](#ormdbalpgsqlbuildinsertstatement) Build the insert statement for $entity -* [delete](#ormdbalpgsqldelete) Delete $entity from database +* [buildCompositeInExpression](#ormdbalpgsqlbuildcompositeinexpression) +* [buildDeleteStatement](#ormdbalpgsqlbuilddeletestatement) +* [buildInsert](#ormdbalpgsqlbuildinsert) Build an insert statement for $rows +* [buildSetClause](#ormdbalpgsqlbuildsetclause) +* [buildUpdateFromStatement](#ormdbalpgsqlbuildupdatefromstatement) +* [buildUpdateStatement](#ormdbalpgsqlbuildupdatestatement) +* [convertJoin](#ormdbalpgsqlconvertjoin) +* [delete](#ormdbalpgsqldelete) Delete rows from $table using $where conditions +* [deleteEntity](#ormdbalpgsqldeleteentity) Delete $entity from database * [describe](#ormdbalpgsqldescribe) Describe a table * [escapeBoolean](#ormdbalpgsqlescapeboolean) Escape a boolean for query * [escapeDateTime](#ormdbalpgsqlescapedatetime) Escape a date time object for query @@ -11674,12 +12252,14 @@ If $values is empty the expression will be `1 = 1` because an empty parenthesis * [escapeString](#ormdbalpgsqlescapestring) Escape a string for query * [escapeValue](#ormdbalpgsqlescapevalue) Returns $value formatted to use in a sql statement. * [extractParenthesis](#ormdbalpgsqlextractparenthesis) Extract content from parenthesis in $type -* [insert](#ormdbalpgsqlinsert) Insert $entities into database +* [insert](#ormdbalpgsqlinsert) * [insertAndSync](#ormdbalpgsqlinsertandsync) Insert $entities and update with default values from database * [insertAndSyncWithAutoInc](#ormdbalpgsqlinsertandsyncwithautoinc) Insert $entities and sync with auto increment primary key +* [insertEntities](#ormdbalpgsqlinsertentities) Insert $entities into database * [normalizeType](#ormdbalpgsqlnormalizetype) Normalize $type * [setOption](#ormdbalpgsqlsetoption) Set $option to $value * [syncInserted](#ormdbalpgsqlsyncinserted) Sync the $entities after insert +* [update](#ormdbalpgsqlupdate) Update $table using $where to set $updates * [updateAutoincrement](#ormdbalpgsqlupdateautoincrement) Update the autoincrement value #### ORM\Dbal\Pgsql::__construct @@ -11732,42 +12312,60 @@ protected static function assertSameType( -#### ORM\Dbal\Pgsql::buildCompositeWhereInStatement +#### ORM\Dbal\Pgsql::buildCompositeInExpression ```php -public function buildCompositeWhereInStatement( - array $cols, array $keys, boolean $inverse = false -): string +public function buildCompositeInExpression( + array $cols, array $values, $inverse = false +) ``` -##### Build a where in statement for composite keys **Visibility:** this method is **public**.
- **Returns**: this method returns **string** -
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `$cols` | **array** | | -| `$keys` | **array** | | -| `$inverse` | **boolean** | Whether it should be a IN or NOT IN operator | +| `$values` | **array** | | +| `$inverse` | | | -#### ORM\Dbal\Pgsql::buildInsertStatement +#### ORM\Dbal\Pgsql::buildDeleteStatement ```php -protected function buildInsertStatement( - ORM\Entity $entity, array<\ORM\Entity> $entities -): string +protected function buildDeleteStatement( $table, array $where ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | + + + +#### ORM\Dbal\Pgsql::buildInsert + +```php +protected function buildInsert( string $table, array $rows ): string ``` -##### Build the insert statement for $entity +##### Build an insert statement for $rows @@ -11780,15 +12378,138 @@ protected function buildInsertStatement( | Parameter | Type | Description | |-----------|------|-------------| -| `$entity` | **\ORM\Entity** | | -| `$entities` | **array<\ORM\Entity>** | | +| `$table` | **string** | | +| `$rows` | **array** | | + + + +#### ORM\Dbal\Pgsql::buildSetClause + +```php +protected function buildSetClause( array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | | + + + +#### ORM\Dbal\Pgsql::buildUpdateFromStatement + +```php +protected function buildUpdateFromStatement( + $table, array $where, array $updates, array $joins +) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | +| `$joins` | **array** | | + + + +#### ORM\Dbal\Pgsql::buildUpdateStatement + +```php +protected function buildUpdateStatement( $table, array $where, array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | + + + +#### ORM\Dbal\Pgsql::convertJoin + +```php +protected function convertJoin( $join ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$join` | | | #### ORM\Dbal\Pgsql::delete ```php -public function delete( ORM\Entity $entity ): boolean +public function delete( string $table, array $where ): integer +``` + +##### Delete rows from $table using $where conditions + +Where conditions can be an array of key => value pairs to check for equality or an array of expressions. + +Examples: +`$dbal->delete('someTable', ['id' => 23])` +`$dbal->delete('user', ['name = \'john\'', 'OR email=\'john.doe@example.com\''])` + +Tip: Use the query builder to construct where conditions: +`$em->query('user')->where('name', 'john')->orWhere('email', '...')->delete();` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table where to delete rows | +| `$where` | **array** | An array of where conditions | + + + +#### ORM\Dbal\Pgsql::deleteEntity + +```php +public function deleteEntity( ORM\Entity $entity ): boolean ``` ##### Delete $entity from database @@ -12037,23 +12758,22 @@ protected function extractParenthesis( string $type ): string #### ORM\Dbal\Pgsql::insert ```php -public function insert( ORM\Entity $entities ): boolean +public function insert( $table, array $rows ) ``` -##### Insert $entities into database -The entities have to be from same type otherwise a InvalidArgument will be thrown. + **Visibility:** this method is **public**.
- **Returns**: this method returns **boolean** -
**Throws:** this method may throw **\ORM\Exception\InvalidArgument**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$entities` | **\ORM\Entity** | | +| `$table` | | | +| `$rows` | **array** | | @@ -12105,6 +12825,29 @@ The entities have to be from same type otherwise a InvalidArgument will be throw +#### ORM\Dbal\Pgsql::insertEntities + +```php +public function insertEntities( ORM\Entity $entities ): boolean +``` + +##### Insert $entities into database + +The entities have to be from same type otherwise a InvalidArgument will be thrown. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **boolean** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$entities` | **\ORM\Entity** | | + + + #### ORM\Dbal\Pgsql::normalizeType ```php @@ -12147,30 +12890,67 @@ public function setOption( string $option, $value ): $this | Parameter | Type | Description | |-----------|------|-------------| -| `$option` | **string** | | -| `$value` | **mixed** | | +| `$option` | **string** | | +| `$value` | **mixed** | | + + + +#### ORM\Dbal\Pgsql::syncInserted + +```php +protected function syncInserted( ORM\Entity $entities ) +``` + +##### Sync the $entities after insert + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$entities` | **\ORM\Entity** | | -#### ORM\Dbal\Pgsql::syncInserted +#### ORM\Dbal\Pgsql::update ```php -protected function syncInserted( ORM\Entity $entities ) +public function update( + string $table, array $where, array $updates, array $joins = array() +): integer ``` -##### Sync the $entities after insert +##### Update $table using $where to set $updates +Simple usage: `update('table', ['id' => 23], ['name' => 'John Doe'])` +For advanced queries with parenthesis, joins (if supported from your DBMS) etc. use QueryBuilder: -**Visibility:** this method is **protected**. -
+```php +$em->query('table') + ->where('birth_date', '>', EM::raw('DATE_SUB(NOW(), INTERVAL 18 YEARS)')) + ->update(['teenager' => true]); +``` +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$entities` | **\ORM\Entity** | | +| `$table` | **string** | The table to update | +| `$where` | **array** | An array of where conditions | +| `$updates` | **array** | An array of columns to update | +| `$joins` | **array** | For internal use from query builder only | @@ -12258,6 +13038,7 @@ Supported: * [column](#ormquerybuilderquerybuildercolumn) Add $column * [columns](#ormquerybuilderquerybuildercolumns) Set $columns * [convertPlaceholders](#ormquerybuilderquerybuilderconvertplaceholders) Replaces question marks in $expression with $args +* [delete](#ormquerybuilderquerybuilderdelete) Execute a delete statement for the current query * [first](#ormquerybuilderquerybuilderfirst) Get the first item of an array * [fullJoin](#ormquerybuilderquerybuilderfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormquerybuilderquerybuildergetdefaultoperator) Get the default operator for $value @@ -12281,6 +13062,7 @@ Supported: * [reset](#ormquerybuilderquerybuilderreset) Reset the position of the cursor to the first row * [rightJoin](#ormquerybuilderquerybuilderrightjoin) Right (outer) join $tableName with $options * [setFetchMode](#ormquerybuilderquerybuildersetfetchmode) Proxy to PDOStatement::setFetchMode() +* [update](#ormquerybuilderquerybuilderupdate) Execute an update statement for the current query * [where](#ormquerybuilderquerybuilderwhere) Alias for andWhere * [whereIn](#ormquerybuilderquerybuilderwherein) Add a where in condition with AND. * [whereNotIn](#ormquerybuilderquerybuilderwherenotin) Add a where not in condition with AND. @@ -12484,6 +13266,24 @@ protected function convertPlaceholders( string $expression, $args ): string +#### ORM\QueryBuilder\QueryBuilder::delete + +```php +public function delete(): integer +``` + +##### Execute a delete statement for the current query + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ + + #### ORM\QueryBuilder\QueryBuilder::first ```php @@ -13046,6 +13846,33 @@ Please note that this will execute the query - further modifications will not ha **See Also:** * \PDOStatement::setFetchMode() +#### ORM\QueryBuilder\QueryBuilder::update + +```php +public function update( array $updates ): integer +``` + +##### Execute an update statement for the current query + +**NOTE:** not all drivers support UPDATE with JOIN (or FROM). Has to be implemented in the database abstraction +layer. + +$updates should be an array which columns to update with what value. Use expressions to bypass escaping. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | An array of columns to update | + + + #### ORM\QueryBuilder\QueryBuilder::where ```php @@ -14265,6 +15092,7 @@ Supported: * [convertPlaceholders](#ormtestingentityfetchermockresultconvertplaceholders) Replaces question marks in $expression with $args * [count](#ormtestingentityfetchermockresultcount) Get the count of the resulting items * [createRelatedJoin](#ormtestingentityfetchermockresultcreaterelatedjoin) Create the join with $join type +* [delete](#ormtestingentityfetchermockresultdelete) Execute a delete statement for the current query * [first](#ormtestingentityfetchermockresultfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockresultfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormtestingentityfetchermockresultgetdefaultoperator) Get the default operator for $value @@ -14296,6 +15124,7 @@ Supported: * [setQuery](#ormtestingentityfetchermockresultsetquery) Set a raw query or use different QueryBuilder * [toClassAndAlias](#ormtestingentityfetchermockresulttoclassandalias) Get class and alias by the match from translateColumn * [translateColumn](#ormtestingentityfetchermockresulttranslatecolumn) Translate attribute names in an expression to their column names +* [update](#ormtestingentityfetchermockresultupdate) Execute an update statement for the current query * [where](#ormtestingentityfetchermockresultwhere) Alias for andWhere * [whereIn](#ormtestingentityfetchermockresultwherein) Add a where in condition with AND. * [whereNotIn](#ormtestingentityfetchermockresultwherenotin) Add a where not in condition with AND. @@ -14585,6 +15414,24 @@ public function createRelatedJoin( $join, $relation ): $this +#### ORM\Testing\EntityFetcherMock\Result::delete + +```php +public function delete(): integer +``` + +##### Execute a delete statement for the current query + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ + + #### ORM\Testing\EntityFetcherMock\Result::first ```php @@ -15329,6 +16176,33 @@ protected function translateColumn( string $expression ): string +#### ORM\Testing\EntityFetcherMock\Result::update + +```php +public function update( array $updates ): integer +``` + +##### Execute an update statement for the current query + +**NOTE:** not all drivers support UPDATE with JOIN (or FROM). Has to be implemented in the database abstraction +layer. + +$updates should be an array which columns to update with what value. Use expressions to bypass escaping. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | An array of columns to update | + + + #### ORM\Testing\EntityFetcherMock\Result::where ```php @@ -15957,7 +16831,6 @@ public function validate( $value ): boolean|ORM\Dbal\Error |------------|------|------|---------------------------------------| | **protected** | `$booleanFalse` | **string** | | | **protected** | `$booleanTrue` | **string** | | -| **protected static** | `$compositeWhereInTemplate` | | | | **protected** | `$entityManager` | ** \ ORM \ EntityManager** | | | **protected** | `$identifierDivider` | **string** | | | **protected** | `$quotingCharacter` | **string** | | @@ -15969,9 +16842,15 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [__construct](#ormdbalsqlite__construct) Dbal constructor. * [assertSameType](#ormdbalsqliteassertsametype) -* [buildCompositeWhereInStatement](#ormdbalsqlitebuildcompositewhereinstatement) Build a where in statement for composite keys -* [buildInsertStatement](#ormdbalsqlitebuildinsertstatement) Build the insert statement for $entity -* [delete](#ormdbalsqlitedelete) Delete $entity from database +* [buildCompositeInExpression](#ormdbalsqlitebuildcompositeinexpression) +* [buildDeleteStatement](#ormdbalsqlitebuilddeletestatement) +* [buildInsert](#ormdbalsqlitebuildinsert) Build an insert statement for $rows +* [buildSetClause](#ormdbalsqlitebuildsetclause) +* [buildUpdateFromStatement](#ormdbalsqlitebuildupdatefromstatement) +* [buildUpdateStatement](#ormdbalsqlitebuildupdatestatement) +* [convertJoin](#ormdbalsqliteconvertjoin) +* [delete](#ormdbalsqlitedelete) Delete rows from $table using $where conditions +* [deleteEntity](#ormdbalsqlitedeleteentity) Delete $entity from database * [describe](#ormdbalsqlitedescribe) Describe a table * [escapeBoolean](#ormdbalsqliteescapeboolean) Escape a boolean for query * [escapeDateTime](#ormdbalsqliteescapedatetime) Escape a date time object for query @@ -15983,13 +16862,15 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [escapeValue](#ormdbalsqliteescapevalue) Returns $value formatted to use in a sql statement. * [extractParenthesis](#ormdbalsqliteextractparenthesis) Extract content from parenthesis in $type * [hasCompositeKey](#ormdbalsqlitehascompositekey) Checks $rawColumns for a multiple primary key -* [insert](#ormdbalsqliteinsert) Insert $entities into database +* [insert](#ormdbalsqliteinsert) * [insertAndSync](#ormdbalsqliteinsertandsync) Insert $entities and update with default values from database * [insertAndSyncWithAutoInc](#ormdbalsqliteinsertandsyncwithautoinc) Insert $entities and sync with auto increment primary key +* [insertEntities](#ormdbalsqliteinsertentities) Insert $entities into database * [normalizeColumnDefinition](#ormdbalsqlitenormalizecolumndefinition) Normalize a column definition * [normalizeType](#ormdbalsqlitenormalizetype) Normalize $type * [setOption](#ormdbalsqlitesetoption) Set $option to $value * [syncInserted](#ormdbalsqlitesyncinserted) Sync the $entities after insert +* [update](#ormdbalsqliteupdate) Update $table using $where to set $updates * [updateAutoincrement](#ormdbalsqliteupdateautoincrement) Update the autoincrement value #### ORM\Dbal\Sqlite::__construct @@ -16042,42 +16923,60 @@ protected static function assertSameType( -#### ORM\Dbal\Sqlite::buildCompositeWhereInStatement +#### ORM\Dbal\Sqlite::buildCompositeInExpression ```php -public function buildCompositeWhereInStatement( - array $cols, array $keys, boolean $inverse = false -): string +public function buildCompositeInExpression( + array $cols, array $values, $inverse = false +) ``` -##### Build a where in statement for composite keys **Visibility:** this method is **public**.
- **Returns**: this method returns **string** -
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `$cols` | **array** | | -| `$keys` | **array** | | -| `$inverse` | **boolean** | Whether it should be a IN or NOT IN operator | +| `$values` | **array** | | +| `$inverse` | | | -#### ORM\Dbal\Sqlite::buildInsertStatement +#### ORM\Dbal\Sqlite::buildDeleteStatement ```php -protected function buildInsertStatement( - ORM\Entity $entity, array<\ORM\Entity> $entities -): string +protected function buildDeleteStatement( $table, array $where ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | + + + +#### ORM\Dbal\Sqlite::buildInsert + +```php +protected function buildInsert( string $table, array $rows ): string ``` -##### Build the insert statement for $entity +##### Build an insert statement for $rows @@ -16090,15 +16989,138 @@ protected function buildInsertStatement( | Parameter | Type | Description | |-----------|------|-------------| -| `$entity` | **\ORM\Entity** | | -| `$entities` | **array<\ORM\Entity>** | | +| `$table` | **string** | | +| `$rows` | **array** | | + + + +#### ORM\Dbal\Sqlite::buildSetClause + +```php +protected function buildSetClause( array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$updates` | **array** | | + + + +#### ORM\Dbal\Sqlite::buildUpdateFromStatement + +```php +protected function buildUpdateFromStatement( + $table, array $where, array $updates, array $joins +) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | +| `$joins` | **array** | | + + + +#### ORM\Dbal\Sqlite::buildUpdateStatement + +```php +protected function buildUpdateStatement( $table, array $where, array $updates ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | | | +| `$where` | **array** | | +| `$updates` | **array** | | + + + +#### ORM\Dbal\Sqlite::convertJoin + +```php +protected function convertJoin( $join ) +``` + + + + +**Visibility:** this method is **protected**. +
+ + +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$join` | | | #### ORM\Dbal\Sqlite::delete ```php -public function delete( ORM\Entity $entity ): boolean +public function delete( string $table, array $where ): integer +``` + +##### Delete rows from $table using $where conditions + +Where conditions can be an array of key => value pairs to check for equality or an array of expressions. + +Examples: +`$dbal->delete('someTable', ['id' => 23])` +`$dbal->delete('user', ['name = \'john\'', 'OR email=\'john.doe@example.com\''])` + +Tip: Use the query builder to construct where conditions: +`$em->query('user')->where('name', 'john')->orWhere('email', '...')->delete();` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of deleted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table where to delete rows | +| `$where` | **array** | An array of where conditions | + + + +#### ORM\Dbal\Sqlite::deleteEntity + +```php +public function deleteEntity( ORM\Entity $entity ): boolean ``` ##### Delete $entity from database @@ -16370,23 +17392,22 @@ protected function hasCompositeKey( array $rawColumns ): boolean #### ORM\Dbal\Sqlite::insert ```php -public function insert( ORM\Entity $entities ): boolean +public function insert( $table, array $rows ) ``` -##### Insert $entities into database -The entities have to be from same type otherwise a InvalidArgument will be thrown. + **Visibility:** this method is **public**.
- **Returns**: this method returns **boolean** -
**Throws:** this method may throw **\ORM\Exception\InvalidArgument**
+ ##### Parameters | Parameter | Type | Description | |-----------|------|-------------| -| `$entities` | **\ORM\Entity** | | +| `$table` | | | +| `$rows` | **array** | | @@ -16438,6 +17459,29 @@ The entities have to be from same type otherwise a InvalidArgument will be throw +#### ORM\Dbal\Sqlite::insertEntities + +```php +public function insertEntities( ORM\Entity $entities ): boolean +``` + +##### Insert $entities into database + +The entities have to be from same type otherwise a InvalidArgument will be thrown. + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **boolean** +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$entities` | **\ORM\Entity** | | + + + #### ORM\Dbal\Sqlite::normalizeColumnDefinition ```php @@ -16534,6 +17578,43 @@ protected function syncInserted( ORM\Entity $entities ) +#### ORM\Dbal\Sqlite::update + +```php +public function update( + string $table, array $where, array $updates, array $joins = array() +): integer +``` + +##### Update $table using $where to set $updates + +Simple usage: `update('table', ['id' => 23], ['name' => 'John Doe'])` + +For advanced queries with parenthesis, joins (if supported from your DBMS) etc. use QueryBuilder: + +```php +$em->query('table') + ->where('birth_date', '>', EM::raw('DATE_SUB(NOW(), INTERVAL 18 YEARS)')) + ->update(['teenager' => true]); +``` + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of affected rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$table` | **string** | The table to update | +| `$where` | **array** | An array of where conditions | +| `$updates` | **array** | An array of columns to update | +| `$joins` | **array** | For internal use from query builder only | + + + #### ORM\Dbal\Sqlite::updateAutoincrement ```php From 51de2e6ace04cf8a1605fa847d90ed031c23b51c Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Thu, 26 Nov 2020 09:13:50 +0100 Subject: [PATCH 11/16] update api reference --- docs/reference.md | 90 ++----------------- .../CompositeInValuesExpression.php | 9 ++ src/QueryBuilder/Parenthesis.php | 9 +- src/QueryBuilder/QueryBuilder.php | 9 +- 4 files changed, 32 insertions(+), 85 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 31f5aff..42a54e3 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1237,7 +1237,6 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [__construct](#ormdbaldbal__construct) Dbal constructor. * [assertSameType](#ormdbaldbalassertsametype) -* [buildCompositeInExpression](#ormdbaldbalbuildcompositeinexpression) * [buildDeleteStatement](#ormdbaldbalbuilddeletestatement) * [buildInsert](#ormdbaldbalbuildinsert) Build an insert statement for $rows * [buildSetClause](#ormdbaldbalbuildsetclause) @@ -1314,31 +1313,6 @@ protected static function assertSameType( -#### ORM\Dbal\Dbal::buildCompositeInExpression - -```php -public function buildCompositeInExpression( - array $cols, array $values, $inverse = false -) -``` - - - - -**Visibility:** this method is **public**. -
- - -##### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$cols` | **array** | | -| `$values` | **array** | | -| `$inverse` | | | - - - #### ORM\Dbal\Dbal::buildDeleteStatement ```php @@ -8749,7 +8723,7 @@ public function setRelated( ORM\Entity $self, $entity = null ) * [__construct](#ormdbalmysql__construct) Dbal constructor. * [assertSameType](#ormdbalmysqlassertsametype) -* [buildCompositeInExpression](#ormdbalmysqlbuildcompositeinexpression) +* [buildCompositeInExpression](#ormdbalmysqlbuildcompositeinexpression) Build a where in expression for composite values * [buildDeleteStatement](#ormdbalmysqlbuilddeletestatement) * [buildInsert](#ormdbalmysqlbuildinsert) Build an insert statement for $rows * [buildSetClause](#ormdbalmysqlbuildsetclause) @@ -8833,16 +8807,18 @@ protected static function assertSameType( ```php public function buildCompositeInExpression( - array $cols, array $values, $inverse = false -) + array $cols, array $values, boolean $inverse = false +): string ``` +##### Build a where in expression for composite values **Visibility:** this method is **public**.
- + **Returns**: this method returns **string** +
##### Parameters @@ -8850,7 +8826,7 @@ public function buildCompositeInExpression( |-----------|------|-------------| | `$cols` | **array** | | | `$values` | **array** | | -| `$inverse` | | | +| `$inverse` | **boolean** | Whether it should be a IN or NOT IN operator | @@ -12233,7 +12209,6 @@ If $values is empty the expression will be `1 = 1` because an empty parenthesis * [__construct](#ormdbalpgsql__construct) Dbal constructor. * [assertSameType](#ormdbalpgsqlassertsametype) -* [buildCompositeInExpression](#ormdbalpgsqlbuildcompositeinexpression) * [buildDeleteStatement](#ormdbalpgsqlbuilddeletestatement) * [buildInsert](#ormdbalpgsqlbuildinsert) Build an insert statement for $rows * [buildSetClause](#ormdbalpgsqlbuildsetclause) @@ -12312,31 +12287,6 @@ protected static function assertSameType( -#### ORM\Dbal\Pgsql::buildCompositeInExpression - -```php -public function buildCompositeInExpression( - array $cols, array $values, $inverse = false -) -``` - - - - -**Visibility:** this method is **public**. -
- - -##### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$cols` | **array** | | -| `$values` | **array** | | -| `$inverse` | | | - - - #### ORM\Dbal\Pgsql::buildDeleteStatement ```php @@ -16842,7 +16792,6 @@ public function validate( $value ): boolean|ORM\Dbal\Error * [__construct](#ormdbalsqlite__construct) Dbal constructor. * [assertSameType](#ormdbalsqliteassertsametype) -* [buildCompositeInExpression](#ormdbalsqlitebuildcompositeinexpression) * [buildDeleteStatement](#ormdbalsqlitebuilddeletestatement) * [buildInsert](#ormdbalsqlitebuildinsert) Build an insert statement for $rows * [buildSetClause](#ormdbalsqlitebuildsetclause) @@ -16923,31 +16872,6 @@ protected static function assertSameType( -#### ORM\Dbal\Sqlite::buildCompositeInExpression - -```php -public function buildCompositeInExpression( - array $cols, array $values, $inverse = false -) -``` - - - - -**Visibility:** this method is **public**. -
- - -##### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$cols` | **array** | | -| `$values` | **array** | | -| `$inverse` | | | - - - #### ORM\Dbal\Sqlite::buildDeleteStatement ```php diff --git a/src/Dbal/QueryLanguage/CompositeInValuesExpression.php b/src/Dbal/QueryLanguage/CompositeInValuesExpression.php index 46ead3b..f576aab 100644 --- a/src/Dbal/QueryLanguage/CompositeInValuesExpression.php +++ b/src/Dbal/QueryLanguage/CompositeInValuesExpression.php @@ -6,6 +6,15 @@ trait CompositeInValuesExpression { use CompositeInExpression; + /** + * Build a where in statement for composite keys + * + * @param array $cols Columns from which to build the composite key + * @param array[] $values Array of composite keys (array) + * @param bool $inverse Whether it should be a IN or NOT IN operator + * @return string + * @internal + */ public function buildCompositeInExpression(array $cols, array $values, $inverse = false) { return '(' . implode(',', $cols) . ') ' . ($inverse ? 'NOT IN' : 'IN') . diff --git a/src/QueryBuilder/Parenthesis.php b/src/QueryBuilder/Parenthesis.php index 2c64f7d..deac9dd 100644 --- a/src/QueryBuilder/Parenthesis.php +++ b/src/QueryBuilder/Parenthesis.php @@ -52,7 +52,14 @@ public function createWhereCondition($column, $operator = null, $value = null) } /** - * {@inheritdoc} + * Build a where in expression + * + * Calls buildWhereInExpression() from parent if there is a parent. + * + * @param string|array $column Column or expression with placeholders + * @param array $values Value (required when used with operator) + * @param bool $inverse + * @return string * @internal */ public function buildWhereInExpression($column, array $values, $inverse = false) diff --git a/src/QueryBuilder/QueryBuilder.php b/src/QueryBuilder/QueryBuilder.php index 0ad35f0..eccd895 100644 --- a/src/QueryBuilder/QueryBuilder.php +++ b/src/QueryBuilder/QueryBuilder.php @@ -156,7 +156,14 @@ public function createWhereCondition($column, $operator = null, $value = null) } /** - * {@inheritdoc} + * Build a where in expression + * + * Calls buildWhereInExpression() from parent if there is a parent. + * + * @param string|array $column Column or expression with placeholders + * @param array $values Array of values + * @param bool $inverse + * @return string * @internal */ public function buildWhereInExpression($column, array $values, $inverse = false) From e36d6bb38f19a5040a2bd3658439e83ffe55c8b5 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Wed, 2 Dec 2020 07:34:10 +0100 Subject: [PATCH 12/16] implement QueryBuilder::insert to insert rows use like this: `$em->query('table')->insert(['col1' => 'val1'], ['col1' => 'val2'])` --- src/QueryBuilder/ExecutesQueries.php | 16 +++++++++++++++- tests/QueryBuilder/BasicTest.php | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php index d85bb25..3d97420 100644 --- a/src/QueryBuilder/ExecutesQueries.php +++ b/src/QueryBuilder/ExecutesQueries.php @@ -128,7 +128,7 @@ public function update(array $updates) } /** - * Execute a delete statement for the current query + * Execute a delete statement on the current table with current where conditions * * @return int The number of deleted rows */ @@ -140,6 +140,20 @@ public function delete() ); } + /** + * Execute a insert statement on the current table + * + * @param array ...$rows + * @return int The number of inserted rows + */ + public function insert(array ...$rows) + { + return $this->entityManager->getDbal()->insert( + EntityManager::raw($this->tableName . ($this->alias ? ' AS ' . $this->alias : '')), + ...$rows + ); + } + /** * Query database and return result * diff --git a/tests/QueryBuilder/BasicTest.php b/tests/QueryBuilder/BasicTest.php index 1679b1b..300ece8 100644 --- a/tests/QueryBuilder/BasicTest.php +++ b/tests/QueryBuilder/BasicTest.php @@ -239,4 +239,21 @@ public function executesDbalDelete() $query->delete(); } + + /** @test */ + public function executesDbalInsert() + { + $query = new QueryBuilder('foo', '', $this->em); + + $this->dbal->shouldReceive('insert')->with( + m::type(Expression::class), + ['col1' => 'val1.1', 'col2' => 'val2.1'], + ['col1' => 'val1.2', 'col2' => 'val2.2'] + )->once()->andReturn(2); + + $query->insert( + ['col1' => 'val1.1', 'col2' => 'val2.1'], + ['col1' => 'val1.2', 'col2' => 'val2.2'] + ); + } } From 640c0cd7a2dfbb3e0ebad20b49069f2dc8032186 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Fri, 4 Dec 2020 13:32:22 +0100 Subject: [PATCH 13/16] translate attributes to column names in entity fetchers update and insert statements --- docs/reference.md | 116 ++++++++++++++++-- src/EntityFetcher.php | 2 + src/EntityFetcher/ExecutesQueries.php | 43 +++++++ src/QueryBuilder/ExecutesQueries.php | 4 +- .../BasicTest.php} | 5 +- .../EntityFetcher/CountTest.php | 2 +- tests/EntityFetcher/ExecutesQueriesTest.php | 36 ++++++ 7 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 src/EntityFetcher/ExecutesQueries.php rename tests/{EntityManager/EntityFetcherTest.php => EntityFetcher/BasicTest.php} (99%) rename tests/{EntityManager => }/EntityFetcher/CountTest.php (97%) create mode 100644 tests/EntityFetcher/ExecutesQueriesTest.php diff --git a/docs/reference.md b/docs/reference.md index 42a54e3..57b134a 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -3502,7 +3502,7 @@ Supported: * [convertPlaceholders](#ormentityfetcherconvertplaceholders) Replaces questionmarks in $expression with $args * [count](#ormentityfetchercount) Get the count of the resulting items * [createRelatedJoin](#ormentityfetchercreaterelatedjoin) Create the join with $join type -* [delete](#ormentityfetcherdelete) Execute a delete statement for the current query +* [delete](#ormentityfetcherdelete) Execute a delete statement on the current table with current where conditions * [first](#ormentityfetcherfirst) Get the first item of an array * [fullJoin](#ormentityfetcherfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormentityfetchergetdefaultoperator) Get the default operator for $value @@ -3512,6 +3512,7 @@ Supported: * [getStatement](#ormentityfetchergetstatement) Query database and return result * [getTableAndAlias](#ormentityfetchergettableandalias) Get the table name and alias for a class * [groupBy](#ormentityfetchergroupby) Group By $column +* [insert](#ormentityfetcherinsert) Execute an insert statement on the current table * [join](#ormentityfetcherjoin) (Inner) join $tableName with $options * [joinRelated](#ormentityfetcherjoinrelated) Join $relation * [leftJoin](#ormentityfetcherleftjoin) Left (outer) join $tableName with $options @@ -3786,7 +3787,7 @@ public function createRelatedJoin( $join, $relation ): $this public function delete(): integer ``` -##### Execute a delete statement for the current query +##### Execute a delete statement on the current table with current where conditions @@ -3995,6 +3996,30 @@ Optionally you can provide an expression in $column with question marks as place +#### ORM\EntityFetcher::insert + +```php +public function insert( array $rows ): integer +``` + +##### Execute an insert statement on the current table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of inserted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$rows` | **array** | | + + + #### ORM\EntityFetcher::join ```php @@ -4716,7 +4741,7 @@ Supported: * [convertPlaceholders](#ormtestingentityfetchermockconvertplaceholders) Replaces question marks in $expression with $args * [count](#ormtestingentityfetchermockcount) Get the count of the resulting items * [createRelatedJoin](#ormtestingentityfetchermockcreaterelatedjoin) Create the join with $join type -* [delete](#ormtestingentityfetchermockdelete) Execute a delete statement for the current query +* [delete](#ormtestingentityfetchermockdelete) Execute a delete statement on the current table with current where conditions * [first](#ormtestingentityfetchermockfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormtestingentityfetchermockgetdefaultoperator) Get the default operator for $value @@ -4726,6 +4751,7 @@ Supported: * [getStatement](#ormtestingentityfetchermockgetstatement) Query database and return result * [getTableAndAlias](#ormtestingentityfetchermockgettableandalias) Get the table name and alias for a class * [groupBy](#ormtestingentityfetchermockgroupby) Group By $column +* [insert](#ormtestingentityfetchermockinsert) Execute an insert statement on the current table * [join](#ormtestingentityfetchermockjoin) (Inner) join $tableName with $options * [joinRelated](#ormtestingentityfetchermockjoinrelated) Join $relation * [leftJoin](#ormtestingentityfetchermockleftjoin) Left (outer) join $tableName with $options @@ -4993,7 +5019,7 @@ public function createRelatedJoin( $join, $relation ): $this public function delete(): integer ``` -##### Execute a delete statement for the current query +##### Execute a delete statement on the current table with current where conditions @@ -5202,6 +5228,30 @@ Optionally you can provide an expression in $column with question marks as place +#### ORM\Testing\EntityFetcherMock::insert + +```php +public function insert( array $rows ): integer +``` + +##### Execute an insert statement on the current table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of inserted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$rows` | **array** | | + + + #### ORM\Testing\EntityFetcherMock::join ```php @@ -12988,7 +13038,7 @@ Supported: * [column](#ormquerybuilderquerybuildercolumn) Add $column * [columns](#ormquerybuilderquerybuildercolumns) Set $columns * [convertPlaceholders](#ormquerybuilderquerybuilderconvertplaceholders) Replaces question marks in $expression with $args -* [delete](#ormquerybuilderquerybuilderdelete) Execute a delete statement for the current query +* [delete](#ormquerybuilderquerybuilderdelete) Execute a delete statement on the current table with current where conditions * [first](#ormquerybuilderquerybuilderfirst) Get the first item of an array * [fullJoin](#ormquerybuilderquerybuilderfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormquerybuilderquerybuildergetdefaultoperator) Get the default operator for $value @@ -12997,6 +13047,7 @@ Supported: * [getQuery](#ormquerybuilderquerybuildergetquery) Get the query / select statement * [getStatement](#ormquerybuilderquerybuildergetstatement) Query database and return result * [groupBy](#ormquerybuilderquerybuildergroupby) Group By $column +* [insert](#ormquerybuilderquerybuilderinsert) Execute an insert statement on the current table * [join](#ormquerybuilderquerybuilderjoin) (Inner) join $tableName with $options * [leftJoin](#ormquerybuilderquerybuilderleftjoin) Left (outer) join $tableName with $options * [limit](#ormquerybuilderquerybuilderlimit) Set $limit @@ -13222,7 +13273,7 @@ protected function convertPlaceholders( string $expression, $args ): string public function delete(): integer ``` -##### Execute a delete statement for the current query +##### Execute a delete statement on the current table with current where conditions @@ -13406,6 +13457,30 @@ Optionally you can provide an expression in $column with question marks as place +#### ORM\QueryBuilder\QueryBuilder::insert + +```php +public function insert( array $rows ): integer +``` + +##### Execute an insert statement on the current table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of inserted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$rows` | **array** | | + + + #### ORM\QueryBuilder\QueryBuilder::join ```php @@ -15042,7 +15117,7 @@ Supported: * [convertPlaceholders](#ormtestingentityfetchermockresultconvertplaceholders) Replaces question marks in $expression with $args * [count](#ormtestingentityfetchermockresultcount) Get the count of the resulting items * [createRelatedJoin](#ormtestingentityfetchermockresultcreaterelatedjoin) Create the join with $join type -* [delete](#ormtestingentityfetchermockresultdelete) Execute a delete statement for the current query +* [delete](#ormtestingentityfetchermockresultdelete) Execute a delete statement on the current table with current where conditions * [first](#ormtestingentityfetchermockresultfirst) Get the first item of an array * [fullJoin](#ormtestingentityfetchermockresultfulljoin) Full (outer) join $tableName with $options * [getDefaultOperator](#ormtestingentityfetchermockresultgetdefaultoperator) Get the default operator for $value @@ -15053,6 +15128,7 @@ Supported: * [getStatement](#ormtestingentityfetchermockresultgetstatement) Query database and return result * [getTableAndAlias](#ormtestingentityfetchermockresultgettableandalias) Get the table name and alias for a class * [groupBy](#ormtestingentityfetchermockresultgroupby) Group By $column +* [insert](#ormtestingentityfetchermockresultinsert) Execute an insert statement on the current table * [join](#ormtestingentityfetchermockresultjoin) (Inner) join $tableName with $options * [joinRelated](#ormtestingentityfetchermockresultjoinrelated) Join $relation * [leftJoin](#ormtestingentityfetchermockresultleftjoin) Left (outer) join $tableName with $options @@ -15370,7 +15446,7 @@ public function createRelatedJoin( $join, $relation ): $this public function delete(): integer ``` -##### Execute a delete statement for the current query +##### Execute a delete statement on the current table with current where conditions @@ -15596,6 +15672,30 @@ Optionally you can provide an expression in $column with question marks as place +#### ORM\Testing\EntityFetcherMock\Result::insert + +```php +public function insert( array $rows ): integer +``` + +##### Execute an insert statement on the current table + + + +**Visibility:** this method is **public**. +
+ **Returns**: this method returns **integer** +
**Response description:** The number of inserted rows +
+ +##### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$rows` | **array** | | + + + #### ORM\Testing\EntityFetcherMock\Result::join ```php diff --git a/src/EntityFetcher.php b/src/EntityFetcher.php index d004c52..c3d07ca 100644 --- a/src/EntityFetcher.php +++ b/src/EntityFetcher.php @@ -2,6 +2,7 @@ namespace ORM; +use ORM\EntityFetcher\ExecutesQueries; use ORM\EntityFetcher\MakesJoins; use ORM\EntityFetcher\TranslatesClasses; use ORM\QueryBuilder\QueryBuilder; @@ -31,6 +32,7 @@ class EntityFetcher extends QueryBuilder { use TranslatesClasses; use MakesJoins; + use ExecutesQueries; /** The entity class that we want to fetch * @var string|Entity */ diff --git a/src/EntityFetcher/ExecutesQueries.php b/src/EntityFetcher/ExecutesQueries.php new file mode 100644 index 0000000..d40dacd --- /dev/null +++ b/src/EntityFetcher/ExecutesQueries.php @@ -0,0 +1,43 @@ +class, 'getColumnName'], array_keys($updates)), + array_values($updates) + ); + return parent::update($updates); + } + + /** + * Execute an insert statement on the current table + * + * @param array ...$rows + * @return int The number of inserted rows + */ + public function insert(array ...$rows) + { + $rows = array_map(function ($row) { + return array_combine( + array_map([$this->class, 'getColumnName'], array_keys($row)), + array_values($row) + ); + }, $rows); + return parent::insert(...$rows); + } +} diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php index 3d97420..4615c89 100644 --- a/src/QueryBuilder/ExecutesQueries.php +++ b/src/QueryBuilder/ExecutesQueries.php @@ -141,7 +141,7 @@ public function delete() } /** - * Execute a insert statement on the current table + * Execute an insert statement on the current table * * @param array ...$rows * @return int The number of inserted rows @@ -149,7 +149,7 @@ public function delete() public function insert(array ...$rows) { return $this->entityManager->getDbal()->insert( - EntityManager::raw($this->tableName . ($this->alias ? ' AS ' . $this->alias : '')), + EntityManager::raw($this->tableName), ...$rows ); } diff --git a/tests/EntityManager/EntityFetcherTest.php b/tests/EntityFetcher/BasicTest.php similarity index 99% rename from tests/EntityManager/EntityFetcherTest.php rename to tests/EntityFetcher/BasicTest.php index 0828e32..b1ccf31 100644 --- a/tests/EntityManager/EntityFetcherTest.php +++ b/tests/EntityFetcher/BasicTest.php @@ -1,6 +1,6 @@ em->fetch(Article::class)->where('someColumn', 42); + + $this->pdo->shouldReceive('query') + ->with('UPDATE "article" AS t0 SET "any_column" = \'foo bar\' WHERE "t0"."some_column" = 42') + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->once()->andReturn(3); + + $fetcher->update(['anyColumn' => 'foo bar']); + } + + /** @test */ + public function insertTranslatesColumnNames() + { + $fetcher = $this->em->fetch(Article::class); + + $this->pdo->shouldReceive('query') + ->with('INSERT INTO "article" ("first_column","second_column") VALUES (\'foo bar\',NULL),(NULL,42)') + ->once()->andReturn($statement = m::mock(\PDOStatement::class)); + $statement->shouldReceive('rowCount')->once()->andReturn(1); + + $fetcher->insert(['firstColumn' => 'foo bar'], ['secondColumn' => 42]); + } +} From 5d2361e6e478827ee38d1f3d3a1685f1dd0ec6d7 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Fri, 4 Dec 2020 13:32:46 +0100 Subject: [PATCH 14/16] check coverage only in php 7.1 --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 41afb1a..861cec5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,11 @@ before_script: script: - composer code-style - - vendor/bin/phpunit -c phpunit.xml --coverage-clover=clover.xml --coverage-text --color=always + - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then + vendor/bin/phpunit -c phpunit.xml --coverage-clover=clover.xml --coverage-text --color=always; + else; + vendor/bin/phpunit -c phpunit.xml --color=always; + fi after_script: - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then From 8f9d15ddc38ad09339ef5ecc623d6b2de9ecf9f6 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Mon, 7 Dec 2020 07:43:53 +0100 Subject: [PATCH 15/16] fix ci script --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 861cec5..416c4ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ script: - composer code-style - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then vendor/bin/phpunit -c phpunit.xml --coverage-clover=clover.xml --coverage-text --color=always; - else; + else vendor/bin/phpunit -c phpunit.xml --color=always; fi From 2060b64389a67b80ea6b49dc7e98517298aa95d1 Mon Sep 17 00:00:00 2001 From: Thomas Flori Date: Fri, 29 Jan 2021 10:06:01 +0100 Subject: [PATCH 16/16] describe how to use query builder --- docs/_data/menu.yml | 1 + docs/entities.md | 25 +- docs/events.md | 4 +- docs/querybuilder.md | 253 +++++++++++++++++++++ docs/reference.md | 18 +- src/QueryBuilder/ExecutesQueries.php | 2 +- src/QueryBuilder/QueryBuilderInterface.php | 2 +- 7 files changed, 283 insertions(+), 22 deletions(-) create mode 100644 docs/querybuilder.md diff --git a/docs/_data/menu.yml b/docs/_data/menu.yml index 2a903ac..5d63521 100644 --- a/docs/_data/menu.yml +++ b/docs/_data/menu.yml @@ -5,6 +5,7 @@ Working With Entities: /entities.html Relation Definition: /relationDefinition.html Working With Relations: /relations.html Events and Observers: /events.html +Use QueryBuilder: /querybuilder.html Validate Data: /validate.html Bulk Inserts: /bulkInserts.html Testing: /testing.html diff --git a/docs/entities.md b/docs/entities.md index d4f7dc5..1d6d00c 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -54,18 +54,25 @@ $entityManager->fetch(User::class, 1); ### Fetching with query builder -By calling `EntityManager::fetch()` and providing only the entity class you will receive an object from -`EntityFetcher`. This class implements the `QueryBuilderInterface` and uses the `QueryBuilder` to delegate the method -calls. The difference between the query builder and the `EntityFetcher` is that the `QueryBuilder` will just return the -query while the `EntityFetcher` will specify the columns to fetch and provides functions to fetch one or more entities. +The query builder is a feature to write sql statements with a fluent interface. You can define where conditions, joins, +group by clause, ordering and even parenthesis for where conditions. You can use this tool to fetch entities by creating +an `EntityFetcher` with `EntityManager::fetch()` and providing the entity class. You can then receive one or all +entities from the query. -Long text short: query builder builds an query (`string QueryBuilder::getQuery()`) and entity fetcher fetches objects of -entities (`Entity EntityFetcher::all()`). +In fact `EntityFetcher` extends the `QueryBuilder` and for more information about building queries please have a look at +the [QueryBuilder](querybuilder.md) documentation. -The `EntityFetcher` does not fetch relations. If you need to fetch a lot of objects and relations consider using more -than one entity fetcher. See the documentation about relations for more details. +Please note that the `EntityFetcher` does not allow fetching other columns than the columns from the main table of +the class, is always distinct and restricts to change the fetch mode. Example usages: -For information how to join, build where conditions and so on please have look in the [API Reference](reference.md). +```php +// fetch a user by $email +$user = $entityManager->fetch(User::class)->where('email', $email)->one(); +// get all articles from last 7 days (mysql) +$articles = $entityManager->fetch(Article::class) + ->where('created', '>', $entityManager::raw('DATE_SUB(NOW(), INTERVAL 7 DAY)')) + ->all(); +``` ### Set and get columns diff --git a/docs/events.md b/docs/events.md index ba86a7b..9d0d029 100644 --- a/docs/events.md +++ b/docs/events.md @@ -3,7 +3,7 @@ layout: default title: Entity Events and Observers permalink: /events.html --- -## Entity Events and Observers +## Entity Events and Observers
(ORM 1 >= 1.9) Entities can be observed using observers. An observer has to implement the `ORM\ObserverInterface` and can be registered in your entity manager for any subclass of `ORM\Entity`. @@ -103,7 +103,7 @@ Entity::observeBy()->on('loadingRelation', function () { }); ``` -### Event Methods +### Event Methods
(ORM 1 >= 1.0) This section describes the methods that get called during the entity livecycle. As they have to be declared in the entity classes you can't define independent observers. Probably all these methods will be declared deprecated in future diff --git a/docs/querybuilder.md b/docs/querybuilder.md new file mode 100644 index 0000000..3a776af --- /dev/null +++ b/docs/querybuilder.md @@ -0,0 +1,253 @@ +--- +layout: default +title: Use QueryBuilder to Interact With Your Database +permalink: /querybuilder.html +--- +## Use QueryBuilder to Interact With Your Database + +You can run queries, updates, inserts and delete statements without the need to create entities for it by generating +queries using the `QueryBuilder`. To get a `QueryBuilder` you just call the query method on your entity manager +and provide the table you want to build a query for: + +```php +/** @var ORM\EntityManager $entityManager */ +$query = $entityManager->query('audit'); +``` + +### Building queries + +In this chapter we want to learn how to use query builders capabilities to create select clause (columns and modifiers), +join clause, where clause, order clause, group clause etc. + +```php +$query = "SELECT $modifiers $columns FROM $table AS $alias $joins " . + "WHERE $conditions GROUP BY $groupColumns ORDER BY $orderColumns " . + "LIMIT $limit OFFSET $offset"; +``` + +#### Columns + +The columns default to `*`. You can add a single column with `$query->column()` or reset the columns with +`$query->columns()`. Once you add a column with `$query->column()` the default asterisk gets removed. + +A column can also be defined as an expression with arguments and alias. + +```php +/** @var ORM\EntityManager $entityManager */ +// return all columns but show 'unknown' for the name column if it is null (mysql) +// SELECT *, IFNULL(name, 'unknown') AS name FROM audit +$entityManager->query('audit') + ->column('*') + ->column('IFNULL(name, ?)', ['unknown'], 'name'); +``` + +#### Where Conditions + +Where conditions are added using the `$query->where()` method. It accepts either a column, operator and value, or an +expression that may contain question marks as placeholders together with an array of values. You can also omit the +operator what results in the operator `=` or `IN` for arrays. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +$query->where('col1', 'LIKE', '%term%'); // "col1 LIKE '%term%'" +$query->where('col2', '<', 23); // "col2 < 23" +$query->where('DATE(col3)', '2020-12-31'); // "DATE(col3) = '2020-12-31'" +$query->where('col4 IS NULL'); // "col4 IS NULL" +$query->where('col5', 'IS', null); // "col5 = 'IS' (CAREFUL! you might not expect this query) +$query->where("IFNULL(col6, ?) <= ?", ['9999-12-31', '2020-08-01']); +``` + +By default, where conditions are combined with `AND` - to combine them with `OR` use the `$query->orWhere()` method. Use +`$query->parenthesis()` or `$query->orParenthesis()` to open a parenthesis. + +Note the logical difference between these two queries: +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +$query->where('a')->where('b')->orWhere('c'); // "a AND b OR c" (c == true would be enough) +$query->where('a')->parenthesis()->where('b')->orWhere('c')->close(); // "a AND (b OR c)" (a == true is required) +``` + +"Where in"-conditions can be written with `->where($col, $values)`, `->where($col, 'NOT IN', $values)` or with +`->whereIn()`, `->whereNotIn()` (and the `->or...` variants). The `->where(Not)In` variants have the advantage that they +also accept an array of columns for combined keys: + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +$query->whereIn(['col1', 'col2'], [['c1v1', 'c2v1'], ['c1v2', 'c2v2']]); +// "(col1, col2) IN (VALUES ('c1v1', 'c2v1'), ('c1v2', 'c2v2'))" +``` + +#### Joins + +Joins can be defined with `$query->join()`, `$query->leftJoin()`, `$query->rightJoin()` and `$query->fullJoin()`. The +methods are all using the same syntax with different join types. + +> Note that there is no `INNER JOIN` nor `FULL OUTER JOIN` as this is the default for `JOIN` and `FULL JOIN`. Also the +> `RIGHT JOIN` and `LEFT JOIN` are outer joins. + +##### 1. Using a single column for joins with USING clause: + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// LEFT JOIN differences USING(auditId) +$query->leftJoin('differences', 'auditId'); +``` + +##### 2. Using an ON clause without arguments + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// JOIN user ON user.id = audit.userId +$query->join('user', 'user.id = audit.userId'); +``` + +##### 3. Using an ON clause with arguments + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// JOIN article ON article.id = audit.entityId AND article.type = 'news' +$query->join('article', 'article.id = comment.articleId AND article.type = ?', ['news']); +``` + +##### 4. Using a parenthesis to define the ON clause + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// RIGHT JOIN article ON (article.id = audit.entityId AND audit.entityType = 'article') +$query->rightJoin('article') + ->where('article.id = audit.entityId') + ->where('audit.entityType', 'article') + ->close(); +``` + +##### 5. An empty join (may throw when executed) + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// JOIN some_table +$query->join('some_table', true); +``` + +> Note that the boolean true is necessary - otherwise you would get a parenthesis + +#### Modifiers, Grouping, Sorting, Offset and Limit + +To add a modifier you call `$query->modifier(string)`. All modifiers are combined with a space between them. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// SELECT SQL_NO_CACHE DISTINCT .... +$query->modifier('SQL_NO_CACHE')->modifier('DISTINCT'); +``` + +For grouping call `$query->groupBy(string, array)`. Group by expressions are combined with a comma between them. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// GROUP BY table.type, table.weight +$query->groupBy('table.type')->groupBy('table.weight'); +``` + +Sorting can be defined with `$query->orderBy()` which accepts an expression with placeholders and the sort direction. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// ORDER BY FIELD(status, 'todo', 'in_progress', 'done') ASC, last_update DESC +$query->orderBy('FIELD(status, ?, ?, ?)', 'ASC', ['todo', 'in_progress', 'done']) + ->orderBy('last_update', 'DESC'); +``` + +You can pass limit and offset using `$query->limit(int)` and `$query->offset(int)`. They are self-explanatory but note +that the offset is ignored when no limit is given. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// LIMIT 10 OFFSET 15 +$query->limit(10)->offset(15); +``` + +#### Limitations of QueryBuilder + +There is no having clause (yet) but you could pass that to the last group by: + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// GROUP BY table.type HAVING COUNT(*) > 10 +$query->groupBy('table.type HAVING COUNT(*) > 10'); +``` + +Be careful passing question marks `?` in string literals as most functions use question marks to replace arguments. +Rather use question marks and pass arguments. + +```php +/** @var ORM\QueryBuilder\QueryBuilder $query */ +// IF(col, ''foo'', ?) -> bad +$query->column('IF(col, \'?\', ?)', ['foo']); +// IF(col, '?', 'foo') -> good +$query->column('IF(col, ?, ?)', ['?', 'foo']); + +// danger! don't do that with user input +$foo = 'foo'; +$query->column("IF(col, '?', '$foo')"); +``` + +### Select statements + +Equal to an entity fetcher you can use the query builder to fetch rows including joins, where conditions, limit and +offset, parenthesis, columns, order and others. + +**receiving rows** + +```php +$query = $entityManager->query('audit'); +$row1 = $query->one(); // first row +$row2 = $query->one(); // second row +$rows = $query->all(); // array of rows starting from 3rd row +$rows = $query->reset()->all(); // array all rows starting from first row +$rows = $query->reset()->one(); // the first row again +``` + +**change the fetch mode** + +```php +$query = $entityManager->query('audit'); +$query->setFetchMode(PDO::FETCH_COLUMN, 0); +$row1Col1 = $query->one(); +$query->setFetchMode(PDO::FETCH_ASSOC); +$row2 = $query->one(); +``` + +> Note that this executes the statement - further modifications will not have any effect + +### Update statements + +You can also use the query builder to execute update statements using the where conditions and joins from the query. + +**updating matching rows** + +```php +$query = $entityManager->query('user'); +$query->where('email', $email)->update(['name' => $name]); +``` + +### Delete Statements + +You can execute delete statements using the defined where conditions on the table. + +```php +$query = $entityManager->query('user'); +$query->where('email', $email)->delete(); +``` + +### Insert Statements + +Insert statements don't use any of the data from the query but still you can execute an insert on the table from +query builder. + +```php +$query = $entityManager->query('user'); +$query->insert( + ['email' => 'john.doe@example.com', 'name' => 'john'], + ['email' => 'jane.doe@example.com', 'name' => 'jane'] +); +``` diff --git a/docs/reference.md b/docs/reference.md index 57b134a..405546b 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -4427,7 +4427,7 @@ can be set to true. ```php public function setFetchMode( - integer $mode, null $classNameObject = null, array $ctorarfg = array() + integer $mode, $classNameObject = null, array $ctorarfg = array() ): $this ``` @@ -4445,7 +4445,7 @@ Please note that this will execute the query - further modifications will not ha | Parameter | Type | Description | |-----------|------|-------------| | `$mode` | **integer** | | -| `$classNameObject` | **null** | | +| `$classNameObject` | **integer | string | object** | | | `$ctorarfg` | **array** | | @@ -5659,7 +5659,7 @@ can be set to true. ```php public function setFetchMode( - integer $mode, null $classNameObject = null, array $ctorarfg = array() + integer $mode, $classNameObject = null, array $ctorarfg = array() ): $this ``` @@ -5677,7 +5677,7 @@ Please note that this will execute the query - further modifications will not ha | Parameter | Type | Description | |-----------|------|-------------| | `$mode` | **integer** | | -| `$classNameObject` | **null** | | +| `$classNameObject` | **integer | string | object** | | | `$ctorarfg` | **array** | | @@ -13845,7 +13845,7 @@ can be set to true. ```php public function setFetchMode( - integer $mode, null $classNameObject = null, array $ctorarfg = array() + integer $mode, $classNameObject = null, array $ctorarfg = array() ): $this ``` @@ -13863,7 +13863,7 @@ Please note that this will execute the query - further modifications will not ha | Parameter | Type | Description | |-----------|------|-------------| | `$mode` | **integer** | | -| `$classNameObject` | **null** | | +| `$classNameObject` | **integer | string | object** | | | `$ctorarfg` | **array** | | @@ -14194,7 +14194,7 @@ public function columns( $columns = null ): $this | Parameter | Type | Description | |-----------|------|-------------| -| `$columns` | | | +| `$columns` | **array | null** | | @@ -16129,7 +16129,7 @@ can be set to true. ```php public function setFetchMode( - integer $mode, null $classNameObject = null, array $ctorarfg = array() + integer $mode, $classNameObject = null, array $ctorarfg = array() ): $this ``` @@ -16147,7 +16147,7 @@ Please note that this will execute the query - further modifications will not ha | Parameter | Type | Description | |-----------|------|-------------| | `$mode` | **integer** | | -| `$classNameObject` | **null** | | +| `$classNameObject` | **integer | string | object** | | | `$ctorarfg` | **array** | | diff --git a/src/QueryBuilder/ExecutesQueries.php b/src/QueryBuilder/ExecutesQueries.php index 4615c89..832c853 100644 --- a/src/QueryBuilder/ExecutesQueries.php +++ b/src/QueryBuilder/ExecutesQueries.php @@ -32,7 +32,7 @@ trait ExecutesQueries * Please note that this will execute the query - further modifications will not have any effect. * * @param int $mode - * @param null $classNameObject + * @param int|string|object $classNameObject * @param array $ctorarfg * @return $this * @see PDOStatement::setFetchMode() diff --git a/src/QueryBuilder/QueryBuilderInterface.php b/src/QueryBuilder/QueryBuilderInterface.php index 2bee9bd..62222f3 100644 --- a/src/QueryBuilder/QueryBuilderInterface.php +++ b/src/QueryBuilder/QueryBuilderInterface.php @@ -16,7 +16,7 @@ interface QueryBuilderInterface extends ParenthesisInterface /** * Set $columns * - * @param $columns + * @param array|null $columns * @return $this */ public function columns(array $columns = null);