diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3a9251f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: byjg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..be68259 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing to byjg/php-micro-orm + +First of all, thank you for taking the time to contribute! + +## How to Contribute + +### Issues + +If you encounter any issues, have questions, or need clarification, please open an issue on our [Issues page](https://github.com/your-repo/issues). This helps us track and prioritize bug fixes and enhancements. + +### Branches + +We have three main branches in this project: + +- **master**: Contains the latest code. It is generally stable, but we recommend using it with caution. +- **a.b**: Use this branch for creating PRs. The naming convention follows `a.b`, where `a` is the major release and `b` is the minor release of the current version. For example, if the current release is 4.9.2, use the branch `4.9` for your PR. You can also use `4.9.x-dev` in your composer for development purposes. +- **future release**: This branch is typically `(a+1).0`. For instance, if the current release is 4.9.2, the future release branch will be `5.0`. + + +### Code Style and Guidelines + +- **Follow PSR Standards**: We follow [PSR-1](https://www.php-fig.org/psr/psr-1/), [PSR-2](https://www.php-fig.org/psr/psr-2/), and [PSR-12](https://www.php-fig.org/psr/psr-12/). +- **Write Clear Commit Messages**: Use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. +- **Documentation**: Update the documentation for any new features or changes. + +### Common Practices + +- **Keep Pull Requests Small**: Smaller PRs are easier to review and merge. Focus on one feature or fix per PR. +- **Write Tests**: Ensure your changes are covered by tests. We aim for a high level of test coverage. +- **Respect Reviewers' Time**: Be responsive to feedback and willing to make necessary changes. + +### Community + +- **Be Respectful**. +- **Collaborate**: We encourage collaboration and open discussion. Don’t hesitate to ask for help or provide feedback. + +Thank you for contributing to byjg/php-micro-orm! Your help is appreciated and makes a big difference. diff --git a/README.md b/README.md index 8d836c1..97a9a94 100644 --- a/README.md +++ b/README.md @@ -56,322 +56,22 @@ These are the key components: * DbDriverIntarce is the implementation to the Database connection. * Repository put all this together -## Examples +## Basics -For the examples below we will use the class 'Users'; +* [Defining the Model](docs/getting-started-model.md) +* [Querying the Database](docs/querying-the-database.md) +* [Updating the database](docs/updating-the-database.md) +* [Using the Mapper Object](docs/using-mapper-object.md) -```php -addFieldMapping(FieldMap::create('createdate')->withFieldName('created')); -``` - -Then you need to create the dataset object and the repository: - -```php -get(10); - -// Persist the entity into the database: -// Will INSERT if does not exists, and UPDATE if exists -$users->name = "New name"; -$repository->save($users); -``` - -### Update Constraints - -You can define a constraint to update a record. -If the constraint is not satisfied the update will not be performed. - -```php -withAllowOnlyNewValuesForFields('name'); - -$users->name = "New name"; -$repository->save($users, $updateConstraint); -``` - -## Advanced uses - -Get a collection using the query object: - -```php -table('users') - ->fields(['id', 'name']) - ->where('name like :part', ['part' => 'A%']); - -// Will return a collection o 'Users' -$collection = $repository->getByQuery($query); -``` - -Returning multiples entities with a query: - -```php -table('order') - ->join('item', 'order.id = item.orderid') - ->where('name like :part', ['part' => 'A%']); - -// Will return a collection of Orders and Items: -// $collection = [ -// [ $order, $item ], -// [ $order, $item ], -// ... -// ]; -$collection = $orderRepository->getByQuery( - $query, - [ - $itemRepository->getMapper() - ] -); -``` - -## Using FieldAlias - -Field alias is an alternate name for a field. This is usefull for disambiguation on join and leftjoin queries. -Imagine in the example above if both tables ITEM and ORDER have the same field called 'ID'. - -In that scenario, the value of ID will be overriden. The solution is use the FieldAlias like below: - -```php -addFieldMapping(FieldMapping::create('id')->withFieldAlias('orderid')); -$itemMapper = new \ByJG\MicroOrm\Mapper(...); -$itemMapper->addFieldMappping(FieldMapping::create('id')->withFieldAlias('itemid')); - -$query = \ByJG\MicroOrm\Query::getInstance() - ->field('order.id', 'orderid') - ->field('item.id', 'itemid') - /* Other fields here */ - ->table('order') - ->join('item', 'order.id = item.orderid') - ->where('name like :part', ['part' => 'A%']); - -// Will return a collection of Orders and Items: -// $collection = [ -// [ $order, $item ], -// [ $order, $item ], -// ... -// ]; -$collection = $orderRepository->getByQuery( - $query, - [ - $itemRepository->getMapper() - ] -); -``` - -You can also add a MAPPER as a Field. In that case the MAPPER will create the field and the correct aliases. - -```php -fields([ - $orderRepository->getMapper(), - $itemRepository->getMapper, - ]); -``` - -## Tables without auto increments fields - -```php -withFieldName('fieldname') - // The field alias of the field in the table. if not defined will use the field name. - ->withFieldAlias('alias') - // Returns the pre-processed value before UPDATE/INSERT the $field name - // If the function returns NULL this field will not be included in the UPDATE/INSERT - ->withUpdateFunction(function ($field, $instance) { - return $field; - }) - // Returns the field value with a post-processed value of $field AFTER query from DB - ->withSelectFunction(function ($field, $instance) { - return $field; - }) - -$mapper->addFieldMapping($fieldMap); -``` - -## Observers - -You can add observers to the repository. -The observer will be called after the insert, update or delete a record in the DB. - -```mermaid -flowchart TD - A[MyRepository] --> |1. addObserver| B[Subject] - C[ObserverdRepository] --> |2. Notify Update| B - B --> |3. Execute Callback| A -``` - -```php -addObserver(new class($this->infoMapper->getTable()) implements ObserverProcessorInterface { - private $table; - - public function __construct($table) - { - $this->table = $table; - } - - public function process(ObserverData $observerData) - { - // Do something here - } - - public function getObserverdTable(): string - { - return $this->table; - } -}); -``` - -The `ObserverData` class contains the following properties: - - `getTable()`: The table name that was affected - - `getEvent()`: The event that was triggered. Can be 'insert', 'update' or 'delete' - - `getData()`: The data that was inserted or updated. It is null in case of delete. - - `getOldData()`: The data before update. In case of insert comes null, and in case of delete comes with the param filters. - - `getRepository()`: The repository is listening to the event (the same as $myRepository) - -*Note*: The observer will not be called if the insert, update or delete is called using the DBDriver object. - -## Using With Recursive SQL Command - -```php -field('start', 1, 'start + 10') - ->field('end', 120, "end - 10") - ->where('start < 100') -; - -$query = \ByJG\MicroOrm\Query::getInstance() - ->withRecursive($recursive) - ->fields(['start', 'end']); - -/* -This will produce the following SQL: - -WITH RECURSIVE test(start, end) AS ( - SELECT 1 as start, 120 as end - UNION ALL SELECT start + 10, end - 10 FROM test WHERE start < 100 -) SELECT start, end FROM test -*/ -``` - -## Pre-defined closures for field map - -### Mapper::defaultClosure($value, $instance) - -Defines the basic behavior for select and update fields; - -### Mapper::doNotUpdateClosure($value, $instance) - -If set in the update field map will make the field not updatable by the micro-orm. -It is usefull for fields that are pre-defined like 'Primary Key'; timestamp fields based on the -update and the creation; and others - -## Before insert and update functions - -You can also set closure to be applied before insert or update a record. -In this case will set in the Repository: - -```php -addRepository($repo1); -$transactionManager->addRepository($repo2); - -// Start the transaction -$transactionManager->beginTransaction(); - -// -// Do some Repository operations with the repo; -// ... - -// commit (or rollback all transactions) -$transactionManager->commitTransaction(); -``` ## Install diff --git a/composer.json b/composer.json index 1ad61bf..62a2649 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "require": { "php": ">=7.4", "ext-json": "*", - "byjg/anydataset-db": "4.9.*" + "byjg/anydataset-db": "4.9.x-dev" }, "require-dev": { "phpunit/phpunit": "5.7.*|7.4.*|^9.6" diff --git a/docs/controlling-the-data.md b/docs/controlling-the-data.md new file mode 100644 index 0000000..ade70b2 --- /dev/null +++ b/docs/controlling-the-data.md @@ -0,0 +1,57 @@ +# Controlling the data + +You can control the data queried or updated by the micro-orm using the Mapper object. + +Let's say you want to store the phone number only with numbers in the database, +but in the entity class, you want to store with the mask. + +You can add the `withUpdateFunction` and `withSelectFunction` to the FieldMap object +as you can see below: + + +```php +withUpdateFunction(function ($field, $instance) { + return preg_replace('/[^0-9]/', '', $field); + }) + // Returns the field value with a post-processed value of $field AFTER query from DB + ->withSelectFunction(function ($field, $instance) { + return preg_replace('/(\d{2})(\d{4})(\d{4})/', '($1) $2-$3', $field); + }) + +$mapper->addFieldMapping($fieldMap); +``` + + +## Pre-defined closures for field map + +### Mapper::defaultClosure($value, $instance) + +Defines the basic behavior for select and update fields; You don't need to set it. Just know it exists. + +### Mapper::doNotUpdateClosure($value, $instance) + +Defines a read-only field. It can be retrieved from the database but will not be updated. + + +## Before insert and update functions + +You can also set closure to be applied before insert or update at the record level and not only in the field level. +In this case will set in the Repository: + +```php +getMapper(); +``` + +## Querying the database + +You can query the database using the repository. + +```php +$myModel = $repository->get(1); +``` + +or + +```php +$query = Query::getInstance() + ->field('name') + ->where('company_id = :cid', ['cid' => 1]); + +$result = $repository->getByQuery($query); +``` \ No newline at end of file diff --git a/docs/observers.md b/docs/observers.md new file mode 100644 index 0000000..9798525 --- /dev/null +++ b/docs/observers.md @@ -0,0 +1,45 @@ +# Observers + +An observer is a class will be called after an insert, update or delete record in the DB. + +```mermaid +flowchart TD + A[MyRepository] --> |1. addObserver| B[Subject] + C[ObserverdRepository] --> |2. Notify Update| B + B --> |3. Execute Callback| A +``` + +## Example + +```php +addObserver(new class($this->infoMapper->getTable()) implements ObserverProcessorInterface { + private $table; + + public function __construct($table) + { + $this->table = $table; + } + + public function process(ObserverData $observerData) + { + // Do something here + } + + public function getObserverdTable(): string + { + return $this->table; + } +}); +``` + +The `ObserverData` class contains the following properties: +- `getTable()`: The table name that was affected +- `getEvent()`: The event that was triggered. Can be 'insert', 'update' or 'delete' +- `getData()`: The data that was inserted or updated. It is null in case of delete. +- `getOldData()`: The data before update. In case of insert comes null, and in case of delete comes with the param filters. +- `getRepository()`: The repository is listening to the event (the same as $myRepository) + +*Note*: The observer will not be called if the insert, update or delete is called using the DBDriver object. + diff --git a/docs/querying-the-database.md b/docs/querying-the-database.md new file mode 100644 index 0000000..c324d5c --- /dev/null +++ b/docs/querying-the-database.md @@ -0,0 +1,196 @@ +# Querying the database + +Once you have the model and the repository, you can query the database using the repository. + +The results will be returned as a collection of the model defined in the repository. + +e.g.: + +```php +$query = \ByJG\MicroOrm\Query::getInstance() + ->table('users') + ->fields(['id', 'name']) + ->where('name like :part', ['part' => 'A%']); + +// Will return a collection o 'Users' +$collection = $repository->getByQuery($query); +``` + +or, you can also get a single record by its ID: + +```php +$myModel = $repository->get(1); +``` + +Some cases you need to query a database using join and want to return a collection of objects from different tables. + +```php +table('order') + ->join('item', 'order.id = item.orderid') + ->where('name like :part', ['part' => 'A%']); + +// Will return a collection of Orders and Items: +// $collection = [ +// [ $order, $item ], +// [ $order, $item ], +// ... +// ]; +$collection = $orderRepository->getByQuery( + $query, + // Add additional mappers to the query + [ + $itemRepository->getMapper() + ] +); + + + +## QueryBasic Object + +The query basic object contains the essential methods to query the database. It can be used +in the `Union` class, and can be converted to a `Updatable` object. + +### Methods + +#### table(string $tableName, string $alias = null) + +The table to query. + +#### fields(array $fields) + +Array of fields to retrieve. if not set, will retrieve all fields. + +e.g.: + +```php +->fields(['id', 'name']) +``` + +#### field(string $field, string $alias = null) + +A single field to retrieve. You can set an alias for the field. + +e.g.: + +```php +->field('username', 'login'); +``` + +#### where(string $where, array $params) + +The where clause. You can use placeholders in the where clause. + +e.g.: + +```php +->where('name like :part', ['part' => 'A%']) +``` + +The placeholders can be named or unnamed. +If you use named placeholders, you need to pass an associative array with the values. + +If you use unnamed placeholders, you need to pass an array with the values in the same order as the placeholders. + +* Named placeholders are defined by a colon followed by the placeholder name; +* Unnamed placeholders are defined by a question mark. The arguments are positional + +#### join(string $table, string $on, string $alias = null) + +Join another table. + +e.g.: + +```php +->table('order', 'o') +->join('item', 'o.id = i.orderid', 'i') +``` + +#### leftJoin(string $table, string $on, string $alias = null) + +Left Join another table. + +#### rightJoin(string $table, string $on, string $alias = null) + +Right Join another table. + +## Query Object + +The Query object extends the QueryBasic object and adds more methods to query the database. + +### Methods + +#### groupBy(array $field) + +Group by a field or an array of fields. + +e.g.: + +```php +->groupBy(['field1', 'field2']) +``` + +#### orderBy(array $field) + +Order by a field or an array of fields. + +e.g.: + +```php +->orderBy(['field1', 'field2']) +``` + +#### limit(int $start, int $pageSize) + +Limit the number of records to retrieve. + +e.g.: + +```php +->limit(10, 20) +``` + +#### top(int $top) + +Get the first N records. + +e.g.: + +```php +->top(10) +``` + +## Union Object + +The Union object is used to combine two queries. Since the Union operation is a set operation, +the queries must have the same fields and the order, group by, and limit must be defined by the +Union object. + +e.g. + +```php +$query1 = \ByJG\MicroOrm\Query::getInstance() + ->table('users') + ->fields(['id', 'name']) + ->where('name like :part', ['part' => 'A%']); + +$query2 = \ByJG\MicroOrm\Query::getInstance() + ->table('customers') + ->fields(['id', 'name']) + ->where('name like :part', ['part' => 'A%']); + +$union = \ByJG\MicroOrm\Union::getInstance() + ->addQuery($query1) + ->addQuery($query2) + ->orderBy(['name']) + ->limit(10, 20); +``` + + + + + + + + diff --git a/docs/tables-without-auto-increment-fields.md b/docs/tables-without-auto-increment-fields.md new file mode 100644 index 0000000..d01a5ed --- /dev/null +++ b/docs/tables-without-auto-increment-fields.md @@ -0,0 +1,18 @@ +# Tables without auto increments fields + +Some tables don't have auto-increment fields, for example, when you have a table with UUID binary. +In this case, you need to provide a function to calculate the unique ID. + +```php +name = "John"; +$user->createdate = new Literal('NOW()'); +$repository->save($user); +``` + +In this example, the `createdate` field will be set to the current date and time based on the database server. + +Note that `$user->createdate = 'NOW()'` will not work because the library will try to escape the value +and will it will treat as string. Instead, you need to use the `Literal` object, as in the example above. + +The same applies when you are doing a query: + +```php +field('name') + ->where('createdate < :date', ['date' => new Literal('NOW()')]); +``` + diff --git a/docs/update-constraints.md b/docs/update-constraints.md new file mode 100644 index 0000000..c020333 --- /dev/null +++ b/docs/update-constraints.md @@ -0,0 +1,28 @@ +# Update Constraints + +An update constraint is a way to define rules to update a record. +If the constraint is not satisfied the update will not be performed. + +```php +withAllowOnlyNewValuesForFields('name'); + +$users->name = "New name"; +$repository->save($users, $updateConstraint); +``` + +## Current Constraints + +### Allow Only New Values for Fields + +This constraint will allow only new values for the fields defined. + +### Custom Constraint + +```php +$updateConstraint = \ByJG\MicroOrm\UpdateConstraint()::instance() + ->withClosure(function($oldInstance, $newInstance) { + return true; // to allow the update, or false to block it. + }); +``` diff --git a/docs/updating-the-database.md b/docs/updating-the-database.md new file mode 100644 index 0000000..3cb8ffa --- /dev/null +++ b/docs/updating-the-database.md @@ -0,0 +1,40 @@ +# Updating the Database + +Once you have defined the model, (see [Getting Started](getting-started-model.md)) you can start to interact with the database +and doing queries, updates, and deletes. + +Update a single record is simple as: + +```php +get(10); +$users->name = "New name"; +$repository->save($users); +``` + +This code will update the record with the ID 10 and set the name to "New name". + +The idea is to insert a new record. If you don't set the ID, the library will assume that you are inserting a new record. + +```php +name = "New name"; +$repository->save($users); +``` + +## Advanced Cases + +In some cases you need to update multiples records at once. See an example: + +```php +table('test'); +$updateQuery->set('fld1', 'A'); +$updateQuery->set('fld2', 'B'); +$updateQuery->set('fld3', 'C'); +$updateQuery->where('fld1 > :id', ['id' => 10]); +``` + +This code will update the table `test` and set the fields `fld1`, `fld2`, and `fld3` to `A`, `B`, and `C` respectively where the `fld1` is greater than 10. \ No newline at end of file diff --git a/docs/using-fieldalias.md b/docs/using-fieldalias.md new file mode 100644 index 0000000..3f2df44 --- /dev/null +++ b/docs/using-fieldalias.md @@ -0,0 +1,47 @@ +# Using FieldAlias + +Field alias is an alternate name for a field. This is useful for disambiguation on join and left join queries. +Imagine in the example above if both tables ITEM and ORDER have the same field called 'ID'. + +In that scenario, the value of ID will be overridden. The solution is use the FieldAlias like below: + +```php +addFieldMapping(FieldMapping::create('id')->withFieldAlias('orderid')); +$itemMapper = new \ByJG\MicroOrm\Mapper(...); +$itemMapper->addFieldMappping(FieldMapping::create('id')->withFieldAlias('itemid')); + +$query = \ByJG\MicroOrm\Query::getInstance() + ->field('order.id', 'orderid') + ->field('item.id', 'itemid') + /* Other fields here */ + ->table('order') + ->join('item', 'order.id = item.orderid') + ->where('name like :part', ['part' => 'A%']); + +// Will return a collection of Orders and Items: +// $collection = [ +// [ $order, $item ], +// [ $order, $item ], +// ... +// ]; +$collection = $orderRepository->getByQuery( + $query, + [ + $itemRepository->getMapper() + ] +); +``` + +You can also add a MAPPER as a Field. In that case the MAPPER will create the field and the correct aliases. + +```php +fields([ + $orderRepository->getMapper(), + $itemRepository->getMapper, + ]); +``` diff --git a/docs/using-mapper-object.md b/docs/using-mapper-object.md new file mode 100644 index 0000000..cffa6b6 --- /dev/null +++ b/docs/using-mapper-object.md @@ -0,0 +1,66 @@ +# Using Mapper Object + +The Mapper object is the main object that you will use to interact with the database and your model. + +For the Majority of the cases you can use the model as you can see in the [Getting Started](getting-started-model.md) section. +This will create the mapper object automatically for you. + +Create a mapper object directly in these scenarios: +- Use a PHP version earlier than 8.0 (soon this will be deprecated in the library) +- You have a Model object you cannot change (like a third-party library) or don't want to change. + +## Creating the Mapper Object + +For the examples below we will use the class 'Users'; + +```php +addFieldMapping(FieldMap::create('createdate')->withFieldName('created')); +``` + +Then you need to create the dataset object and the repository: + +```php +get(10); + +// Persist the entity into the database: +// Will INSERT if does not exists, and UPDATE if exists +$users->name = "New name"; +$repository->save($users); +``` + + diff --git a/docs/using-with-recursive-sql-command.md b/docs/using-with-recursive-sql-command.md new file mode 100644 index 0000000..6ca4b0b --- /dev/null +++ b/docs/using-with-recursive-sql-command.md @@ -0,0 +1,23 @@ +# Using With Recursive SQL Command + +```php +field('start', 1, 'start + 10') + ->field('end', 120, "end - 10") + ->where('start < 100') +; + +$query = \ByJG\MicroOrm\Query::getInstance() + ->withRecursive($recursive) + ->fields(['start', 'end']); + +/* +This will produce the following SQL: + +WITH RECURSIVE test(start, end) AS ( + SELECT 1 as start, 120 as end + UNION ALL SELECT start + 10, end - 10 FROM test WHERE start < 100 +) SELECT start, end FROM test +*/ +``` diff --git a/src/DeleteQuery.php b/src/DeleteQuery.php new file mode 100644 index 0000000..62eee72 --- /dev/null +++ b/src/DeleteQuery.php @@ -0,0 +1,43 @@ +getWhere(); + if (is_null($whereStr)) { + throw new InvalidArgumentException('You must specifiy a where clause'); + } + + $sql = 'DELETE FROM ' . $this->table + . ' WHERE ' . $whereStr[0]; + + $params = array_merge($params, $whereStr[1]); + + return ORMHelper::processLiteral($sql, $params); + } + + public function convert(?DbFunctionsInterface $dbDriver = null): QueryBuilderInterface + { + $query = Query::getInstance() + ->table($this->table); + + foreach ($this->where as $item) { + $query->where($item['filter'], $item['params']); + } + + return $query; + } +} diff --git a/src/InsertQuery.php b/src/InsertQuery.php new file mode 100644 index 0000000..56aada8 --- /dev/null +++ b/src/InsertQuery.php @@ -0,0 +1,86 @@ +fields(['name', 'price']); + * + * @param array $fields + * @return $this + */ + public function fields(array $fields) + { + $this->fields = array_merge($this->fields, (array)$fields); + + return $this; + } + + protected function getFields() + { + if (empty($this->fields)) { + return ' * '; + } + + return ' ' . implode(', ', $this->fields) . ' '; + } + + /** + * @param $params + * @param DbFunctionsInterface|null $dbHelper + * @return null|string|string[] + * @throws \ByJG\MicroOrm\Exception\OrmInvalidFieldsException + */ + public function build(&$params, DbFunctionsInterface $dbHelper = null) + { + if (empty($this->fields)) { + throw new OrmInvalidFieldsException('You must specifiy the fields for insert'); + } + + $fieldsStr = $this->fields; + if (!is_null($dbHelper)) { + $fieldsStr = $dbHelper->delimiterField($fieldsStr); + } + + $tableStr = $this->table; + if (!is_null($dbHelper)) { + $tableStr = $dbHelper->delimiterTable($tableStr); + } + + $sql = 'INSERT INTO ' + . $tableStr + . '( ' . implode(', ', $fieldsStr) . ' ) ' + . ' values ' + . '( [[' . implode(']], [[', $this->fields) . ']] ) '; + + return ORMHelper::processLiteral($sql, $params); + } + + public function convert(?DbFunctionsInterface $dbDriver = null): QueryBuilderInterface + { + $query = Query::getInstance() + ->fields($this->fields) + ->table($this->table); + + foreach ($this->where as $item) { + $query->where($item['filter'], $item['params']); + } + + return $query; + } +} diff --git a/src/Repository.php b/src/Repository.php index c67b57f..ec0e942 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -4,6 +4,7 @@ use ByJG\AnyDataset\Db\DbDriverInterface; use ByJG\MicroOrm\Exception\OrmBeforeInvalidException; +use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; use ByJG\MicroOrm\Exception\RepositoryReadOnlyException; use ByJG\Serializer\BinderObject; use ByJG\Serializer\SerializerObject; @@ -139,7 +140,7 @@ public function get($pkId) public function delete($pkId) { [$filterList, $filterKeys] = $this->getPkFilter($pkId); - $updatable = Updatable::getInstance() + $updatable = DeleteQuery::getInstance() ->table($this->mapper->getTable()) ->where($filterList, $filterKeys); @@ -147,14 +148,14 @@ public function delete($pkId) } /** - * @param \ByJG\MicroOrm\Updatable $updatable + * @param UpdateBuilderInterface $updatable * @return bool - * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException + * @throws RepositoryReadOnlyException */ - public function deleteByQuery(Updatable $updatable) + public function deleteByQuery(DeleteQuery $updatable) { $params = []; - $sql = $updatable->buildDelete($params); + $sql = $updatable->build($params); $this->getDbDriverWrite()->execute($sql, $params); @@ -310,18 +311,21 @@ public function save($instance, UpdateConstraint $updateConstraint = null) } $isInsert = empty($oldInstance); - // Prepare query to insert - $updatable = Updatable::getInstance() - ->table($this->mapper->getTable()) - ->fields(array_keys($array)); - // Execute Before Statements if ($isInsert) { $closure = $this->beforeInsert; $array = $closure($array); + $updatable = InsertQuery::getInstance() + ->table($this->mapper->getTable()) + ->fields(array_keys($array)); } else { $closure = $this->beforeUpdate; $array = $closure($array); + $updatable = UpdateQuery::getInstance() + ->table($this->mapper->getTable()); + foreach ($array as $field => $value) { + $updatable->set($field, $value); + } } // Check if is OK @@ -360,12 +364,13 @@ public function addObserver(ObserverProcessorInterface $observerProcessor) } /** - * @param \ByJG\MicroOrm\Updatable $updatable + * @param $instance + * @param UpdateBuilderInterface $updatable * @param array $params * @return int - * @throws \ByJG\MicroOrm\Exception\OrmInvalidFieldsException + * @throws OrmInvalidFieldsException */ - protected function insert($instance, Updatable $updatable, array $params) + protected function insert($instance, InsertQuery $updatable, array $params) { $keyGen = $this->getMapper()->generateKey($instance); if (empty($keyGen)) { @@ -376,39 +381,39 @@ protected function insert($instance, Updatable $updatable, array $params) } /** - * @param \ByJG\MicroOrm\Updatable $updatable + * @param UpdateBuilderInterface $updatable * @param array $params * @return int - * @throws \ByJG\MicroOrm\Exception\OrmInvalidFieldsException + * @throws RepositoryReadOnlyException */ - protected function insertWithAutoinc(Updatable $updatable, array $params) + protected function insertWithAutoinc(InsertQuery $updatable, array $params) { - $sql = $updatable->buildInsert($params, $this->getDbDriverWrite()->getDbHelper()); + $sql = $updatable->build($params, $this->getDbDriverWrite()->getDbHelper()); $dbFunctions = $this->getDbDriverWrite()->getDbHelper(); return $dbFunctions->executeAndGetInsertedId($this->getDbDriverWrite(), $sql, $params); } /** - * @param \ByJG\MicroOrm\Updatable $updatable + * @param UpdateBuilderInterface $updatable * @param array $params * @param $keyGen * @return mixed - * @throws \ByJG\MicroOrm\Exception\OrmInvalidFieldsException + * @throws RepositoryReadOnlyException */ - protected function insertWithKeyGen(Updatable $updatable, array $params, $keyGen) + protected function insertWithKeyGen(InsertQuery $updatable, array $params, $keyGen) { $params[$this->mapper->getPrimaryKey()[0]] = $keyGen; - $sql = $updatable->buildInsert($params, $this->getDbDriverWrite()->getDbHelper()); + $sql = $updatable->build($params, $this->getDbDriverWrite()->getDbHelper()); $this->getDbDriverWrite()->execute($sql, $params); return $keyGen; } /** - * @param \ByJG\MicroOrm\Updatable $updatable + * @param UpdateBuilderInterface $updatable * @param array $params - * @throws \ByJG\MicroOrm\Exception\InvalidArgumentException + * @throws RepositoryReadOnlyException */ - protected function update(Updatable $updatable, array $params) + protected function update(UpdateQuery $updatable, array $params) { $fields = array_map(function ($item) use ($params) { return $params[$item]; @@ -417,7 +422,7 @@ protected function update(Updatable $updatable, array $params) [$filterList, $filterKeys] = $this->getPkFilter($fields); $updatable->where($filterList, $filterKeys); - $sql = $updatable->buildUpdate($params, $this->getDbDriverWrite()->getDbHelper()); + $sql = $updatable->build($params, $this->getDbDriverWrite()->getDbHelper()); $this->getDbDriverWrite()->execute($sql, $params); } diff --git a/src/Union.php b/src/Union.php index 999241f..d544669 100644 --- a/src/Union.php +++ b/src/Union.php @@ -47,6 +47,13 @@ public function orderBy(array $fields): Union return $this; } + public function groupBy(array $fields): Union + { + $this->queryAgreggation->groupBy($fields); + + return $this; + } + /** * @param $start * @param $end diff --git a/src/Updatable.php b/src/Updatable.php index 9b60916..5f64126 100644 --- a/src/Updatable.php +++ b/src/Updatable.php @@ -2,35 +2,16 @@ namespace ByJG\MicroOrm; +use ByJG\AnyDataset\Db\DbDriverInterface; use ByJG\AnyDataset\Db\DbFunctionsInterface; use ByJG\MicroOrm\Exception\InvalidArgumentException; use ByJG\MicroOrm\Exception\OrmInvalidFieldsException; -class Updatable +abstract class Updatable implements UpdateBuilderInterface { - protected $fields = []; protected $table = ""; protected $where = []; - public static function getInstance() - { - return new Updatable(); - } - - /** - * Example: - * $query->fields(['name', 'price']); - * - * @param array $fields - * @return $this - */ - public function fields(array $fields) - { - $this->fields = array_merge($this->fields, (array)$fields); - - return $this; - } - /** * Example * $query->table('product'); @@ -59,14 +40,6 @@ public function where($filter, array $params = []) return $this; } - protected function getFields() - { - if (empty($this->fields)) { - return ' * '; - } - - return ' ' . implode(', ', $this->fields) . ' '; - } protected function getWhere() { @@ -84,96 +57,4 @@ protected function getWhere() return [ implode(' AND ', $whereStr), $params ]; } - - - /** - * @param $params - * @param DbFunctionsInterface|null $dbHelper - * @return null|string|string[] - * @throws \ByJG\MicroOrm\Exception\OrmInvalidFieldsException - */ - public function buildInsert(&$params, DbFunctionsInterface $dbHelper = null) - { - if (empty($this->fields)) { - throw new OrmInvalidFieldsException('You must specifiy the fields for insert'); - } - - $fieldsStr = $this->fields; - if (!is_null($dbHelper)) { - $fieldsStr = $dbHelper->delimiterField($fieldsStr); - } - - $tableStr = $this->table; - if (!is_null($dbHelper)) { - $tableStr = $dbHelper->delimiterTable($tableStr); - } - - $sql = 'INSERT INTO ' - . $tableStr - . '( ' . implode(', ', $fieldsStr) . ' ) ' - . ' values ' - . '( [[' . implode(']], [[', $this->fields) . ']] ) '; - - return ORMHelper::processLiteral($sql, $params); - } - - /** - * @param array $params - * @param DbFunctionsInterface|null $dbHelper - * @return string - * @throws InvalidArgumentException - */ - public function buildUpdate(&$params, DbFunctionsInterface $dbHelper = null) - { - if (empty($this->fields)) { - throw new InvalidArgumentException('You must specifiy the fields for insert'); - } - - $fieldsStr = []; - foreach ($this->fields as $field) { - $fieldName = $field; - if (!is_null($dbHelper)) { - $fieldName = $dbHelper->delimiterField($fieldName); - } - $fieldsStr[] = "$fieldName = [[$field]] "; - } - - $whereStr = $this->getWhere(); - if (is_null($whereStr)) { - throw new InvalidArgumentException('You must specifiy a where clause'); - } - - $tableName = $this->table; - if (!is_null($dbHelper)) { - $tableName = $dbHelper->delimiterTable($tableName); - } - - $sql = 'UPDATE ' . $tableName . ' SET ' - . implode(', ', $fieldsStr) - . ' WHERE ' . $whereStr[0]; - - $params = array_merge($params, $whereStr[1]); - - return ORMHelper::processLiteral($sql, $params); - } - - /** - * @param array $params - * @return string - * @throws InvalidArgumentException - */ - public function buildDelete(&$params) - { - $whereStr = $this->getWhere(); - if (is_null($whereStr)) { - throw new InvalidArgumentException('You must specifiy a where clause'); - } - - $sql = 'DELETE FROM ' . $this->table - . ' WHERE ' . $whereStr[0]; - - $params = array_merge($params, $whereStr[1]); - - return ORMHelper::processLiteral($sql, $params); - } } diff --git a/src/UpdateBuilderInterface.php b/src/UpdateBuilderInterface.php new file mode 100644 index 0000000..a63fc69 --- /dev/null +++ b/src/UpdateBuilderInterface.php @@ -0,0 +1,13 @@ +set[$field] = $value; + return $this; + } + + /** + * @param array $params + * @param DbFunctionsInterface|null $dbHelper + * @return string + * @throws InvalidArgumentException + */ + public function build(&$params, DbFunctionsInterface $dbHelper = null) + { + if (empty($this->set)) { + throw new InvalidArgumentException('You must specifiy the fields for update'); + } + + $fieldsStr = []; + foreach ($this->set as $field => $value) { + $fieldName = $field; + if (!is_null($dbHelper)) { + $fieldName = $dbHelper->delimiterField($fieldName); + } + $fieldsStr[] = "$fieldName = [[$field]] "; + $params[$field] = $value; + } + + $whereStr = $this->getWhere(); + if (is_null($whereStr)) { + throw new InvalidArgumentException('You must specifiy a where clause'); + } + + $tableName = $this->table; + if (!is_null($dbHelper)) { + $tableName = $dbHelper->delimiterTable($tableName); + } + + $sql = 'UPDATE ' . $tableName . ' SET ' + . implode(', ', $fieldsStr) + . ' WHERE ' . $whereStr[0]; + + $params = array_merge($params, $whereStr[1]); + + return ORMHelper::processLiteral($sql, $params); + } + public function convert(?DbFunctionsInterface $dbDriver = null): QueryBuilderInterface + { + $query = Query::getInstance() + ->fields(array_keys($this->set)) + ->table($this->table); + + foreach ($this->where as $item) { + $query->where($item['filter'], $item['params']); + } + + return $query; + } +} diff --git a/tests/DeleteQueryTest.php b/tests/DeleteQueryTest.php new file mode 100644 index 0000000..fb7d8d5 --- /dev/null +++ b/tests/DeleteQueryTest.php @@ -0,0 +1,112 @@ +object = new DeleteQuery(); + } + + protected function tearDown(): void + { + $this->object = null; + } + + public function testDelete() + { + $this->object->table('test'); + $this->object->where('fld1 = [[id]]', ['id' => 10]); + + $params = []; + $sql = $this->object->build($params); + $this->assertEquals( + [ + 'DELETE FROM test WHERE fld1 = [[id]]', + [ 'id' => 10 ] + ], + [ + $sql, + $params + ] + + ); + } + + public function testDeleteError() + { + $this->expectException(InvalidArgumentException::class); + + $params = []; + + $this->object->table('test'); + $this->object->build($params); + } + + public function testQueryUpdatable() + { + $this->object->table('test'); + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld2 = :teste', [ 'teste' => 10 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test WHERE fld2 = :teste', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld3 = 20'); + + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test WHERE fld2 = :teste AND fld3 = 20', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld1 = [[teste2]]', [ 'teste2' => 40 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test WHERE fld2 = :teste AND fld3 = 20 AND fld1 = [[teste2]]', + 'params' => [ 'teste' => 10, 'teste2' => 40 ] + ], + $this->object->convert()->build() + ); + } + +} diff --git a/tests/InsertQueryTest.php b/tests/InsertQueryTest.php new file mode 100644 index 0000000..41e1bd5 --- /dev/null +++ b/tests/InsertQueryTest.php @@ -0,0 +1,120 @@ +object = new InsertQuery(); + } + + protected function tearDown(): void + { + $this->object = null; + } + + public function testInsert() + { + $this->object->table('test'); + + $this->object->fields(['fld1']); + $this->object->fields(['fld2', 'fld3']); + + $params = []; + $sql = $this->object->build($params); + $this->assertEquals( + [ + 'INSERT INTO test( fld1, fld2, fld3 ) values ( [[fld1]], [[fld2]], [[fld3]] ) ', + [] + ], + [ + $sql, + $params + ] + ); + + $params = []; + $sql = $this->object->build($params, new DbSqliteFunctions()); + $this->assertEquals( + [ + 'INSERT INTO `test`( `fld1`, `fld2`, `fld3` ) values ( [[fld1]], [[fld2]], [[fld3]] ) ', + [] + ], + [ + $sql, + $params + ] + ); + } + + public function testQueryUpdatable() + { + $this->object->table('test'); + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + + $this->object + ->fields(['fld1']) + ->fields(['fld2', 'fld3']); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld2 = :teste', [ 'teste' => 10 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld3 = 20'); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste AND fld3 = 20', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld1 = [[teste2]]', [ 'teste2' => 40 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste AND fld3 = 20 AND fld1 = [[teste2]]', + 'params' => [ 'teste' => 10, 'teste2' => 40 ] + ], + $this->object->convert()->build() + ); + } + +} diff --git a/tests/RepositoryTest.php b/tests/RepositoryTest.php index 1818208..446023e 100644 --- a/tests/RepositoryTest.php +++ b/tests/RepositoryTest.php @@ -4,6 +4,7 @@ use ByJG\AnyDataset\Db\DbDriverInterface; use ByJG\AnyDataset\Db\Factory; +use ByJG\MicroOrm\DeleteQuery; use ByJG\MicroOrm\Exception\AllowOnlyNewValuesConstraintException; use ByJG\MicroOrm\Exception\RepositoryReadOnlyException; use ByJG\MicroOrm\FieldMapping; @@ -406,7 +407,7 @@ public function testDeleteLiteral() public function testDelete2() { - $query = Updatable::getInstance() + $query = DeleteQuery::getInstance() ->table($this->userMapper->getTable()) ->where('name like :name', ['name'=>'Jane%']); diff --git a/tests/UnionTest.php b/tests/UnionTest.php index 8b634b2..d6870bc 100644 --- a/tests/UnionTest.php +++ b/tests/UnionTest.php @@ -51,6 +51,19 @@ public function testAddQueryWithOrderBy() $this->assertEquals(["name" => 'a%', 'price' => 10], $build["params"]); } + public function testAddQueryWithGroupBy() + { + $union = new Union(); + $union->addQuery(QueryBasic::getInstance()->table("table1")->fields(['name', 'price'])->where('name like :name', ['name' => 'a%'])); + $union->addQuery(QueryBasic::getInstance()->table("table2")->fields(['name', 'price'])->where('price > :price', ['price' => 10])); + $union->groupBy(['name']); + + $build = $union->build(Factory::getDbRelationalInstance(new Uri('sqlite:///tmp/teste.db'))); + + $this->assertEquals("SELECT name, price FROM table1 WHERE name like :name UNION SELECT name, price FROM table2 WHERE price > :price GROUP BY name", $build["sql"]); + $this->assertEquals(["name" => 'a%', 'price' => 10], $build["params"]); + } + public function testInvalidArgument() { $this->expectException(InvalidArgumentException::class); diff --git a/tests/UpdatableTest.php b/tests/UpdatableTest.php deleted file mode 100644 index 0b63c8e..0000000 --- a/tests/UpdatableTest.php +++ /dev/null @@ -1,139 +0,0 @@ -object = new Updatable(); - } - - protected function tearDown(): void - { - $this->object = null; - } - - public function testInsert() - { - $this->object->table('test'); - - $this->object->fields(['fld1']); - $this->object->fields(['fld2', 'fld3']); - - $params = []; - $sql = $this->object->buildInsert($params); - $this->assertEquals( - [ - 'INSERT INTO test( fld1, fld2, fld3 ) values ( [[fld1]], [[fld2]], [[fld3]] ) ', - [] - ], - [ - $sql, - $params - ] - ); - - $params = []; - $sql = $this->object->buildInsert($params, new DbSqliteFunctions()); - $this->assertEquals( - [ - 'INSERT INTO `test`( `fld1`, `fld2`, `fld3` ) values ( [[fld1]], [[fld2]], [[fld3]] ) ', - [] - ], - [ - $sql, - $params - ] - ); - } - - public function testUpdate() - { - $this->object->table('test'); - - $this->object->fields(['fld1']); - $this->object->fields(['fld2', 'fld3']); - - $this->object->where('fld1 = [[id]]', ['id' => 10]); - - $params = []; - $sql = $this->object->buildUpdate($params); - $this->assertEquals( - [ - 'UPDATE test SET fld1 = [[fld1]] , fld2 = [[fld2]] , fld3 = [[fld3]] WHERE fld1 = [[id]]', - [ 'id' => 10 ] - ], - [ - $sql, - $params - ] - ); - - $params = []; - $sql = $this->object->buildUpdate($params, new DbSqliteFunctions()); - $this->assertEquals( - [ - 'UPDATE `test` SET `fld1` = [[fld1]] , `fld2` = [[fld2]] , `fld3` = [[fld3]] WHERE fld1 = [[id]]', - [ 'id' => 10 ] - ], - [ - $sql, - $params - ] - ); - } - - public function testUpdateError() - { - $this->expectException(InvalidArgumentException::class); - - $this->object->table('test'); - - $this->object->fields(['fld1']); - $this->object->fields(['fld2', 'fld3']); - - $params = []; - $this->object->buildUpdate($params); - } - - public function testDelete() - { - $this->object->table('test'); - $this->object->where('fld1 = [[id]]', ['id' => 10]); - - $params = []; - $sql = $this->object->buildDelete($params); - $this->assertEquals( - [ - 'DELETE FROM test WHERE fld1 = [[id]]', - [ 'id' => 10 ] - ], - [ - $sql, - $params - ] - - ); - } - - public function testDeleteError() - { - $this->expectException(InvalidArgumentException::class); - - $params = []; - - $this->object->table('test'); - $this->object->buildDelete($params); - } -} diff --git a/tests/UpdateQueryTest.php b/tests/UpdateQueryTest.php new file mode 100644 index 0000000..5e12aac --- /dev/null +++ b/tests/UpdateQueryTest.php @@ -0,0 +1,138 @@ +object = new UpdateQuery(); + } + + protected function tearDown(): void + { + $this->object = null; + } + + public function testUpdate() + { + $this->object->table('test'); + + $this->object->set('fld1', 'A'); + $this->object->set('fld2', 'B'); + $this->object->set('fld3', 'C'); + + $this->object->where('fld1 = [[id]]', ['id' => 10]); + + $params = []; + $sql = $this->object->build($params); + $this->assertEquals( + [ + 'UPDATE test SET fld1 = [[fld1]] , fld2 = [[fld2]] , fld3 = [[fld3]] WHERE fld1 = [[id]]', + [ 'id' => 10, 'fld1' => 'A', 'fld2' => 'B', 'fld3' => 'C' ] + ], + [ + $sql, + $params + ] + ); + + $params = []; + $sql = $this->object->build($params, new DbSqliteFunctions()); + $this->assertEquals( + [ + 'UPDATE `test` SET `fld1` = [[fld1]] , `fld2` = [[fld2]] , `fld3` = [[fld3]] WHERE fld1 = [[id]]', + [ 'id' => 10, 'fld1' => 'A', 'fld2' => 'B', 'fld3' => 'C' ] + ], + [ + $sql, + $params + ] + ); + } + + public function testUpdateError() + { + $this->expectException(InvalidArgumentException::class); + + $this->object->table('test'); + + $this->object->set('fld1', 'A'); + $this->object->set('fld2', 'B'); + $this->object->set('fld3', 'C'); + + $params = []; + $this->object->build($params); + } + + public function testQueryUpdatable() + { + $this->object->table('test'); + $this->assertEquals( + [ + 'sql' => 'SELECT * FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + + $this->object + ->set('fld1', 'A') + ->set('fld2', 'B') + ->set('fld3', 'C'); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test', + 'params' => [] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld2 = :teste', [ 'teste' => 10 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld3 = 20'); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste AND fld3 = 20', + 'params' => [ 'teste' => 10 ] + ], + $this->object->convert()->build() + ); + + $this->object + ->where('fld1 = [[teste2]]', [ 'teste2' => 40 ]); + + $this->assertEquals( + [ + 'sql' => 'SELECT fld1, fld2, fld3 FROM test WHERE fld2 = :teste AND fld3 = 20 AND fld1 = [[teste2]]', + 'params' => [ 'teste' => 10, 'teste2' => 40 ] + ], + $this->object->convert()->build() + ); + } + +}