├── .gitignore
├── .idea
├── .gitignore
├── Orm.iml
├── codeStyles
│ └── codeStyleConfig.xml
├── deployment.xml
├── encodings.xml
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── php-test-framework.xml
├── php.xml
└── vcs.xml
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── docs
├── behaviours.md
├── cookbook_custom_entities.md
├── cookbook_polymorphic_associations.md
├── cookbook_table_inheritance.md
├── cookbook_testing.md
├── cookbook_validation.md
├── couscous.yml
├── direct_queries.md
├── entities.md
├── index.md
├── limits.md
├── mappers.md
├── queries.md
├── relation_aggregate.md
├── relation_eav.md
├── relation_many_to_many.md
├── relation_many_to_one.md
├── relation_one_to_many.md
├── relation_one_to_one.md
├── relations.md
├── the_actions.md
├── the_casting_manager.md
├── the_guards.md
├── the_query_scopes.md
└── the_tracker.md
├── phpunit.mysql.xml
├── phpunit.xml
├── src
├── Action
│ ├── ActionInterface.php
│ ├── AttachEntities.php
│ ├── BaseAction.php
│ ├── Delete.php
│ ├── DeletePivotRows.php
│ ├── DetachEntities.php
│ ├── Insert.php
│ ├── SoftDelete.php
│ └── Update.php
├── Behaviour
│ ├── BehaviourInterface.php
│ ├── SoftDelete.php
│ └── Timestamps.php
├── CastingManager.php
├── Collection
│ ├── Collection.php
│ └── PaginatedCollection.php
├── Connection.php
├── ConnectionLocator.php
├── Entity
│ ├── Behaviours.php
│ ├── EntityInterface.php
│ ├── GenericEntity.php
│ ├── GenericEntityHydrator.php
│ ├── HydratorInterface.php
│ ├── LazyAggregate.php
│ ├── LazyLoader.php
│ ├── LazyRelation.php
│ ├── StateEnum.php
│ └── Tracker.php
├── Exception
│ └── FailedActionException.php
├── Helpers
│ ├── Arr.php
│ ├── Inflector.php
│ ├── QueryHelper.php
│ └── Str.php
├── Mapper.php
├── MapperConfig.php
├── Orm.php
├── Query.php
├── QueryBuilder.php
└── Relation
│ ├── Aggregate.php
│ ├── HasAggregates.php
│ ├── ManyToMany.php
│ ├── ManyToOne.php
│ ├── OneToMany.php
│ ├── OneToOne.php
│ ├── Relation.php
│ ├── RelationBuilder.php
│ └── RelationConfig.php
└── tests
├── Action
├── DeleteTest.php
├── FakeThrowsException.php
├── InsertTest.php
└── UpdateTest.php
├── BaseTestCase.php
├── Behaviour
├── FakeThrowsException.php
├── SoftDeleteTest.php
└── TimestampsTest.php
├── CastingManagerTest.php
├── Entity
├── FakeEntity.php
└── ProductEntity.php
├── Helpers
├── ArrTest.php
└── StrTest.php
├── MapperTest.php
├── OrmTest.php
├── QueryTest.php
├── Relation
├── ManyToManyTest.php
├── ManyToOneTest.php
├── OneToManyTest.php
├── OneToOneTest.php
└── RelationTest.php
├── benchmark
├── AbstractTestSuite.php
├── SiriusOrmTestSuite.php
├── TestRunner.php
└── sfTimer.php
├── bootstrap.php
└── resources
├── mappers
├── categories.php
├── content_products.php
├── images.php
├── products.php
└── tags.php
└── tables
├── generic.php
└── mysql.php
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | build/
3 | php-cs-fixer.phar
4 | phpcbf.phar
5 | phpcs.phar
6 | .couscous/
7 | couscous.phar
8 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /workspace.xml
--------------------------------------------------------------------------------
/.idea/Orm.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/deployment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/php-test-framework.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - 7.2
6 | - 7.3
7 | - 7.4
8 |
9 | matrix:
10 | allow_failures:
11 | - php: 7.1
12 |
13 | before_script:
14 | - wget -c https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar
15 | - composer self-update
16 | - composer install --prefer-source
17 |
18 | script:
19 | - mkdir -p build/logs
20 | - composer run-script cs
21 | - composer run-script test
22 |
23 | after_script:
24 | - wget https://scrutinizer-ci.com/ocular.phar
25 | - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Adrian Miu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://github.com/siriusphp/orm)
3 | [](https://github.com/siriusphp/orm/releases)
4 | [](https://github.com/siriusphp/orm/blob/master/LICENSE)
5 | [](https://travis-ci.org/siriusphp/orm)
6 | [](https://scrutinizer-ci.com/g/siriusphp/orm/code-structure)
7 | [](https://scrutinizer-ci.com/g/siriusphp/orm)
8 |
9 | Sirius ORM is a [fast and lightweight](https://github.com/adrianmiu/forked-php-orm-benchmark) yet flexible data mapper solution developed with DX in mind. It offers:
10 |
11 | 1. Mapping rows to your own entities
12 | 2. Relations and relation aggregates (COUNT/AVERAGE)
13 | 3. Eager-loading & lazy-loading (without increasing the number of queries)
14 | 4. Queries that let you JOIN with relations (not tables)
15 | 5. Deep persistence
16 | 6. Dynamically defined mappers
17 | 7. Speed & low memory usage (no Entity Manager)
18 | 8. 90+% code coverage
19 |
20 | ### Installation
21 |
22 | ```
23 | composer require siriusphp/orm
24 | ```
25 |
26 | ### Initialization
27 |
28 | ```php
29 | use Sirius\Orm\Orm;
30 | use Sirius\Orm\ConnectionLocator;
31 | $connectionLocator = ConnectionLocator::new(
32 | 'mysql:host=localhost;dbname=testdb',
33 | 'username',
34 | 'password'
35 | );
36 | $orm = new Orm($connectionLocator);
37 | ```
38 |
39 | ### Configuration
40 |
41 | AKA, registering mappers and relations
42 |
43 | ```php
44 | $orm->register('pages', MapperConfig::fromArray([
45 | /**
46 | * here goes the configuration
47 | */
48 | ]));
49 |
50 | // continue with the rest of mappers
51 | ```
52 |
53 | ### Usage
54 |
55 | ```php
56 | // find by ID
57 | $page = $orm->find('pages', 1);
58 | // or via the mapper
59 | $page = $orm->get('pages')->find(1);
60 |
61 | // query
62 | $pages = $orm->select('pages')
63 | ->where('status', 'published')
64 | ->orderBy('date desc')
65 | ->limit(10)
66 | ->get();
67 |
68 | // manipulate
69 | $page->title = 'Best ORM evah!';
70 | $page->featured_image->path = 'orm_schema.png';
71 |
72 | // persist
73 | $orm->save($page);
74 | // or via the mapper
75 | $orm->get('pages')->save($page);
76 | ```
77 |
78 | ### Links
79 |
80 | - [Documentation](https://www.sirius.ro/php/sirius/orm/)
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "siriusphp/orm",
3 | "description": "Powerfull and fast PDO-based data mapper",
4 | "type": "library",
5 | "license": "MIT",
6 | "keywords": [
7 | "orm",
8 | "datamapper",
9 | "not-activerecord",
10 | "pdo",
11 | "modelling",
12 | "entity framework",
13 | "sql",
14 | "data",
15 | "mapping"
16 | ],
17 | "authors": [
18 | {
19 | "name": "Adrian Miu",
20 | "email": "adrian@adrianmiu.ro"
21 | }
22 | ],
23 | "minimum-stability": "stable",
24 | "require": {
25 | "php": ">=7.1",
26 | "atlas/pdo": "^1.1",
27 | "siriusphp/sql": "^1.2",
28 | "doctrine/collections": "^1.6",
29 | "symfony/inflector": "^4.4"
30 | },
31 | "require-dev": {
32 | "phpunit/phpunit": "^8.5",
33 | "mockery/mockery": "^1.3"
34 | },
35 | "autoload": {
36 | "psr-4": {
37 | "Sirius\\Orm\\": "src/"
38 | }
39 | },
40 | "autoload-dev": {
41 | "psr-4": {
42 | "Sirius\\Orm\\Tests\\": "tests/"
43 | }
44 | },
45 | "scripts": {
46 | "cs": [
47 | "php phpcs.phar --standard=PSR2 ./src"
48 | ],
49 | "cbf": [
50 | "php phpcbf.phar ./src --standard=PSR2 -w"
51 | ],
52 | "csfix": [
53 | "php php-cs-fixer.phar fix ./src --rules=@PSR2"
54 | ],
55 | "build-docs": [
56 | "php couscous.phar generate --target=build/docs/ ./docs"
57 | ],
58 | "docs": [
59 | "cd docs && php ../couscous.phar preview"
60 | ],
61 | "test": [
62 | "vendor/bin/phpunit -c phpunit.xml"
63 | ]
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/docs/behaviours.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Behaviours | Sirius ORM
3 | ---
4 |
5 | # Behaviours
6 |
7 | Behaviours are objects that alter the... behaviours of mappers.
8 |
9 | The are attached to the mapper's definition:
10 |
11 | ```php
12 | $orm->register('products', MapperConfig::fromArray([
13 | // other mapper config goes here
14 | 'behaviours' => [new SoftDelete('deleted_at')]
15 | ]));
16 | ```
17 |
18 | or on the fly:
19 |
20 | ```php
21 | $orm->get('products')->use(new SoftDelete('deleted_at'));
22 | ```
23 |
24 | You can disable temporarily one behaviour like this:
25 |
26 | ```php
27 | $orm->get('products')->without('soft_delete')
28 | ->newQuery();
29 | ```
30 |
31 | This would clone the mapper and let you work with it under the new configuration. However the registered mapper instance will still be there on the next `$orm->get('products')`
32 |
33 | The Sirius ORM comes with 2 behaviours:
34 |
35 | > ##### Soft Delete
36 |
37 | > ```php
38 | > $orm->get('products')
39 | > ->use(new SoftDelete('name of the column with the date of delete'));
40 | > ```
41 |
42 | > ##### Timestamps
43 |
44 | > ```php
45 | > $orm->get('products')
46 | > ->use(new Timestamps('column for create', 'column for update'));
47 | > ```
48 |
49 | ## Temporarily disable behaviours
50 |
51 | Behaviours should be active all the times but sometimes you need to disable them for a short period of time. In the case of Soft Deletes you may want to query entities that were deleted. If you want o restore an entity you can do the following:
52 |
53 | ```php
54 | $allProductsMapper = $orm->get('products')->without('soft_deletes');
55 | $deletedProduct = $allProductsMapper->find($id);
56 | $deletedProduct->deleted_at = null;
57 | $allProductMapper->save($deletedProduct);
58 | ```
59 |
60 | ## Create your own behaviours
61 |
62 | Behaviours can intercept the result of various methods in the mapper and can alter that result or provide a new one.
63 |
64 | For example, the `SoftDelete` behaviour intercepts the `Delete` action that is generated when you call `$mapper->delete($entity)` and returns another action of class `SoftDelete` which performs an update. It also intercepts new queries and sets a
65 | guard `['deleted_at' => null]`.
66 |
67 | The mechanism by which this happens is this: when a mapper's method is called and it is allowed to be intercepted by behaviours the mappers will try to call the `on[method]` methods on the attached behaviours.
68 |
69 | The `SoftDelete` behaviours implements `onDelete()` (for the mapper's `delete` method) and `onNewQuery` (for the mapper's `newQuery` method).
70 |
71 | Below is the list of methods that can be intercepted by behaviours:
72 |
73 | - `newEntity` - used to create a new entity (from an array or a table rows)
74 | - `extractFromEntity` - used to extract the columns that have to be persisted
75 | - `save`
76 | - `delete`
77 | - `newQuery`
78 |
79 | Check out the `SoftDelete` and `Timestamps` classes for inspiration
--------------------------------------------------------------------------------
/docs/cookbook_custom_entities.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook - Custom entities | Sirius ORM
3 | ---
4 |
5 | # Cookbook - Custom entities
6 |
7 | Out of the box the Sirius ORM provides a `GenericEntity` class to get you started as soon as possible.
8 |
9 | It allows you to work with entities like you would a plain object:
10 |
11 | ```php
12 | $product->title = 'New product';
13 | echo $product->category->name; // where category could be eager or lazy loaded
14 | ```
15 |
16 | If you prefer another style like using getters and setters or whatever here's what you have to do:
17 |
18 | #### 1. Create the entity class
19 |
20 | - it has to implement the `EntityInterface` interface. Sorry, another one of those nasty trade-offs.
21 | - implement a way to set/retrieve properties on the entity instance
22 |
23 | Here's an example for `Category` entity that uses getters and setters
24 |
25 | ```php
26 | use Sirius\Orm\Entity\EntityInterface;
27 |
28 | class Category implements EntityInterface
29 | {
30 | protected $id;
31 | protected $name;
32 | protected $parent_id;
33 | protected $parent;
34 | protected $products;
35 |
36 | protected $_state;
37 |
38 | protected $_changes;
39 |
40 | public function getId()
41 | {
42 | return $this->id;
43 | }
44 |
45 | public function setId($val){
46 | $this->id = $val;
47 | }
48 |
49 | public function getPersistenceState() {
50 | return $this->_state;
51 | }
52 |
53 | public function setPersistenceState($state) {
54 | $this->_state = $state;
55 | }
56 |
57 | public function getArrayCopy() {
58 | return [
59 | 'id' => $this->id,
60 | 'name' => $this->name,
61 | 'parent_id' => $this->parent_id,
62 | 'parent' => $this->parent ? $this->parent->getArrayCopy() : null,
63 | // ... you get the idea
64 | ];
65 | }
66 |
67 | public function getChanges() {
68 | return $this->_changes;
69 | }
70 |
71 | // getters and setters
72 | public function getName() {
73 | return $this->name;
74 | }
75 |
76 | public function setName($name) {
77 | $current = $this->name;
78 | $this->name = $name;
79 | if ($name !== $current) {
80 | $this->_changes['name'] = true;
81 | }
82 | }
83 | }
84 | ```
85 |
86 | #### 2. Create the entity hydrator class
87 |
88 | The hydrator for this entity should be able to transform an array (representing the row in the DB) and build a new entity
89 |
90 | ```php
91 | use Sirius\Orm\Entity\HydratorInterface;
92 | use Sirius\Orm\Entity\EntityInterface;
93 |
94 | class CategoryHydrator implements HydratorInterface {
95 |
96 | public function hydrate($attributes = []){
97 | $category = new Category;
98 | $category->setId($attributes['id']);
99 | $category->setName($attributes['name']);
100 | $category->setParentId($attributes['parent_id']);
101 | }
102 |
103 | public function extract(EntityInterface $entity) {
104 | /**
105 | * Extract entity attributes that are to be persisted to the database
106 | */
107 | }
108 |
109 | public function set($entity, $attribute, $value) {
110 | // set a single attribute
111 | }
112 |
113 | public function get($entity, $attribute) {
114 | // get a single attribute
115 | }
116 | }
117 | ```
118 |
119 | Check out `GenericEntityHydrator` class for inspiration.
120 |
121 | #### 3. Create a mapper class
122 |
123 | For this you need to alter the behavior of `setEntityAttribute` and `getEntityAttribute` methods
124 |
125 | ```php
126 | use Sirius\Orm\Mapper;
127 |
128 | class CategoryMapper extends Mapper {
129 | protected $table = 'category';
130 | protected $primaryKey = 'id';
131 | // here goes the rest of the mapper's properties
132 |
133 | public function setEntityAttribute(EntityInterface $entity, $attribute, $value)
134 | {
135 | $setter = 'set_' . $attribute;
136 | return $entity->{$setter}($value);
137 | }
138 |
139 | public function getEntityAttribute(EntityInterface $entity, $attribute)
140 | {
141 | $getter = 'get_' . $attribute;
142 | return $entity->{$getter}();
143 | }
144 |
145 | }
146 | ```
147 |
148 | #### 4. Register the mapper in the ORM using a factory
149 |
150 | ```php
151 | $orm->register('categories', function($orm) {
152 | return new CategoryMapper($orm, new CategoryHydrator());
153 | });
154 | ```
155 |
156 | If your mappers are complex and you are using a DiC you can do this:
157 |
158 | ```php
159 | $orm->register('categories', function($orm) use ($di) {
160 | return new CategoryMapper($orm, new CategoryHydrator(), $di->get('someService'));
161 | });
162 | ```
163 |
164 | **Warning!** The Sirius ORM internals makes some assumptions about the results of various values returned by entity methods:
165 |
166 | 1. One-to-many and many-to-many relations are attached to entities as Collections (which extend Doctrine's ArrayCollection class)
167 | 2. `getChanges()` is used to determine the changes to be persisted. Since there is no Entity Manager to track them, the entity is in responsible for tracking the changes.
168 |
169 |
--------------------------------------------------------------------------------
/docs/cookbook_polymorphic_associations.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook - Polymorphic associations | Sirius ORM
3 | ---
4 |
5 | # Cookbook - Polymorphic associations
6 |
7 | There are situations where you want to use a single table to store one type of entities (eg: comments) that related to multiple other types of entities (products, blog posts etc) which reside in different tables.
8 | In this example the `comments` table would have to include, beside the ID of the attached entity (eg: product) an additional column (eg: `commentable_type`) that refers to the type of entity that is linked to.
9 | This column is called a "discriminator" column.
10 |
11 | While other ORMs use the concept of "morphs" for this type of relations we try to keep things as simple as possible so this can be achieved using regular relations and [guards](the_guards).
12 |
13 |
14 | ## Solution 1. Using guards on relations
15 |
16 | This is how you would define the "one-to-many" relation between a product and it's comments where the `comments` table is also used by other entities
17 |
18 | ```php
19 | use Sirius\Orm\MapperConfig;
20 | use Sirius\Orm\Relation\RelationConfig;
21 |
22 | $productsConfig = MapperConfig::fromArray([
23 | MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
24 | MapperConfig::TABLE => 'products',
25 | MapperConfig::RELATIONS => [
26 | 'comments' => [
27 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
28 | RelationConfig::FOREIGN_MAPPER => 'comments', // name of the comments mapper as registered in the ORM
29 | RelationConfig::FOREIGN_KEY => 'commentable_id',
30 | RelationConfig::FOREIGN_GUARDS => ['commentable_type' => 'product'], // That's it!
31 | ]
32 | ]
33 | ]);
34 | $orm->register('products', $productsConfig);
35 | ```
36 |
37 | After this set up all queries made on the products' related comments will have the guards added and any comment related to a product that is persisted to the database will have it's `commentable_type` column automatically set to 'product'
38 |
39 | ## Solution 2. Using guards on mappers
40 |
41 | For this solution you define a specific mapper for the "Product Comments" which contains the guards and make the products have many "product comments", like so:
42 |
43 | ```php
44 | use Sirius\Orm\MapperConfig;
45 | use Sirius\Orm\Relation\RelationConfig;
46 |
47 | $productCommentsConfig = MapperConfig::fromArray([
48 | MapperConfig::ENTITY_CLASS => 'App\Entity\ProductComment',
49 | MapperConfig::TABLE => 'comments',
50 | MapperConfig::GUARDS => ['commentable_type' => 'product']
51 | ]);
52 |
53 | $productsConfig = MapperConfig::fromArray([
54 | MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
55 | MapperConfig::TABLE => 'products',
56 | MapperConfig::RELATIONS => [
57 | 'comments' => [
58 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
59 | RelationConfig::FOREIGN_MAPPER => 'product_comments',
60 | RelationConfig::FOREIGN_KEY => 'commentable_id'
61 | ]
62 | ]
63 | ]);
64 | $orm->register('product_comments', $productCommentsConfig);
65 | $orm->register('products', $productsConfig);
66 | ```
67 |
68 | This solution relies on the mapper to ensure the proper value is set on the `commentable_type` column at the time it is persisted.
--------------------------------------------------------------------------------
/docs/cookbook_table_inheritance.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook - table inheritance | Sirius ORM
3 | ---
4 |
5 | # Cookbook - Table inheritance
6 |
7 | Sometimes you need to use the same table to store multiple type of entities. If you are familiar with Wordpress, the `posts` table stores pages, posts and custom content types.
8 |
9 | ## Solution 1. One mapper per entity type
10 |
11 | If you don't want the mapper to query multiple entity types you can use [guards](the_guards.md) you configure the mapper like this
12 |
13 | ```php
14 | $pageConfig = MapperConfig::fromArray([
15 | MapperConfig::ENTITY_CLASS => 'App\Entity\Content\Page',
16 | MapperConfig::TABLE => 'content',
17 | MapperConfig::GUARDS => ['content_type' => 'page']
18 | ]);
19 | $orm->register('pages', $pageConfig);
20 | ```
21 |
22 | ## Solution 2. Custom entity hydrator
23 |
24 | If you want to query the mapper for all types of content BUT you want the rows in the table to be converted into different entity types you need a custom Hydrator.
25 |
26 | ```php
27 | use Sirius\Orm\GenericEntityHydrator;
28 |
29 | class CustomHydrator extends GenericEntityHydrator {
30 | public function hydrate($arr) {
31 | if ($arr['content_type'] == 'Post') {
32 | return new Page($arr);
33 | }
34 | return new Content($arr);
35 | }
36 | }
37 | ```
38 |
39 | ```php
40 | $customEntityHydrator = new CustomHydrator;
41 |
42 | $contentConfig = MapperConfig::fromArray([
43 | MapperConfig::ENTITY_CLASS => 'App\Entity\Content',
44 | MapperConfig::TABLE => 'content',
45 | MapperConfig::ENTITY_FACTORY => $customEntityHydrator
46 | ]);
47 | $orm->register('content', $contentConfig);
48 | ```
49 |
50 | In this case the `$customEntityFactoryInstance` will be in charge of converting a row into the proper type of content
51 |
52 | **Warning!** This solution poses some problems
53 |
54 | 1. Since relations are defined on a per-mapper basis, all the relations attached to the "content" mapper are considered to be applicable for all generated entities (pages, posts, products).
55 | Thus, the ORM will try to attach relations where you might not want to (eg: pages don't have many "price_rules" like products may have)
56 | 2. You are in charge of setting the `content_type` attribute on the entity so persistence works properly
57 | 3. Before persisting an entity the mapper checks if it can do it by invoking `assertCanPersistEntity()`.
58 | If you are not using a custom mapper to overwrite this method it will check if the entity that's about to be persisted has the same class as
59 | the `$mapper->entityClass` property. So you would need to make sure that your entity classes have the proper inheritance structure.
60 |
61 | For this reason we recommend using guards to create specific mappers and a general-purpose mapper that allows you to interact with the entities in a "unified" way.
--------------------------------------------------------------------------------
/docs/cookbook_testing.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook - Testing | Sirius ORM
3 | ---
4 |
5 | # Cookbook - Testing
6 |
7 | TBD
--------------------------------------------------------------------------------
/docs/cookbook_validation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cookbook - Entity validation | Sirius ORM
3 | ---
4 |
5 | # Cookbook - Entity validation
6 |
7 | TBD
--------------------------------------------------------------------------------
/docs/couscous.yml:
--------------------------------------------------------------------------------
1 | template:
2 | url: https://github.com/siriusphp/Template-ReadTheDocs
3 |
4 | # List of directories to exclude from the processing (default contains "vendor" and "website")
5 | # Paths are relative to the repository root
6 | exclude:
7 | - website
8 | - vendor
9 | - test
10 | - src
11 | - build
12 |
13 | # Base URL of the published website (no "/" at the end!)
14 | # You are advised to set and use this variable to write your links in the HTML layouts
15 | baseUrl: https://www.sirius.ro/php/sirius/orm
16 | paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SGXDKNJCXFPJU
17 | gacode: UA-535999-18
18 |
19 | projectName: Sirius\Orm
20 | title: Sirius\Orm
21 | subTitle: Fast and flexible PHP ORM solution built with DX in mind
22 |
23 | # The left menu bar
24 | menu:
25 | sections:
26 | # versions:
27 | # name: Versions
28 | # items:
29 | # two:
30 | # text: "2.0"
31 | # relativeUrl:
32 | # one:
33 | # text: "1.0"
34 | # relativeUrl: 1_0/
35 | guide:
36 | name: Getting started
37 | items:
38 | getting_started:
39 | text: Introduction
40 | relativeUrl:
41 | mappers:
42 | text: Mappers
43 | relativeUrl: mappers.html
44 | insert:
45 | text: Querying entities
46 | relativeUrl: queries.html
47 | update:
48 | text: Working with entities
49 | relativeUrl: entities.html
50 | delete:
51 | text: Behaviours
52 | relativeUrl: behaviours.html
53 | other:
54 | text: Direct queries
55 | relativeUrl: direct_queries.html
56 | query_scopes:
57 | text: Query scopes
58 | relativeUrl: the_query_scopes.html
59 | limits:
60 | text: Limitations
61 | relativeUrl: limits.html
62 | relations:
63 | name: Relations
64 | items:
65 | getting_started:
66 | text: Basics
67 | relativeUrl: relations.html
68 | one_to_one:
69 | text: One-to-one
70 | relativeUrl: relation_one_to_one.html
71 | one_to_many:
72 | text: One-to-many
73 | relativeUrl: relation_one_to_many.html
74 | many_to_one:
75 | text: Many-to-one
76 | relativeUrl: relation_many_to_one.html
77 | many_to_many:
78 | text: Many-to-many
79 | relativeUrl: relation_many_to_many.html
80 | eav:
81 | text: EAV
82 | relativeUrl: relation_eav.html
83 | aggregatges:
84 | text: Aggregates
85 | relativeUrl: relation_aggregate.html
86 | cookbook:
87 | name: Cookbook
88 | items:
89 | custom_entities:
90 | text: Custom entities
91 | relativeUrl: cookbook_custom_entities.html
92 | table-inheritance:
93 | text: Table inheritance
94 | relativeUrl: cookbook_table_inheritance.html
95 | polymorphic_associations:
96 | text: Polymorphic associations
97 | relativeUrl: cookbook_polymorphic_associations.html
98 | validation:
99 | text: Entity validation
100 | relativeUrl: cookbook_validation.html
101 | testing:
102 | text: Testing
103 | relativeUrl: cookbook_testing.html
104 | advanced:
105 | name: Advanced
106 | items:
107 | guards:
108 | text: The guards
109 | relativeUrl: the_guards.html
110 | actions:
111 | text: The actions
112 | relativeUrl: the_actions.html
113 | tracker:
114 | text: The tracker
115 | relativeUrl: the_tracker.html
116 | casting:
117 | text: The casting manager
118 | relativeUrl: the_casting_manager.html
--------------------------------------------------------------------------------
/docs/direct_queries.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Direct queries | Sirius ORM
3 | ---
4 |
5 | # Direct queries
6 |
7 | Sometimes you want to execute queries on the mapper but you don't care for entities. For example you might want to count the number of comments per a list of articles, or get the average rating for some products
8 |
9 | Constructing queries work the same as normal queries except the last part where you ask for the results
10 |
11 | ```php
12 | $orm->get('reviews')
13 | ->join('INNER', 'products', 'products.id = reviews.product_id')
14 | ->where('products.name', 'Gold', 'contains')
15 | ->groupBy('product_id')
16 | ->select('product_id', 'AVERAGE(rating) as rating')
17 | ->fetchKeyPair();
18 | ```
19 |
20 | Since Sirius\ORM uses Sirius\Sql which in turn uses Atlas\Connection which are glorified PDO connections the methods available on PDO statements for retrieving results are also available on the ORM queries:
21 |
22 | - `fetchAll`
23 | - `fetchColumn`(int $column = 0)
24 | - `fetchGroup`(int $style = PDO::FETCH_COLUMN)
25 | - `fetchKeyPair`()
26 | - `fetchObject`(string $class = 'stdClass', array $args = [])
27 | - `fetchObjects`(string $class = 'stdClass', array $args = [])
28 | - `fetchOne`()
29 | - `fetchUnique`()
30 | - `fetchValue`(int $column = 0)
31 |
32 | Here's another example for counting some matching rows:
33 |
34 | ```php
35 | $orm->get('reviews')
36 | ->join('INNER', 'products', 'products.id = reviews.product_id')
37 | ->where('products.name', 'Gold', 'contains')
38 | ->select('COUNT(reviews.id) as total_gold_reviews')
39 | ->fetchValue();
40 | ```
41 |
42 | ## Reusing queries
43 |
44 | You can reuse queries to minimise the potential for errors:
45 |
46 | ```php
47 | $query = $orm->get('reviews')
48 | ->join('INNER', 'products', 'products.id = reviews.product_id')
49 | ->where('products.name', 'Gold', 'contains')
50 | ->groupBy('product_id');
51 |
52 | $ratings = $query->select('product_id', 'AVERAGE(rating) as rating')->fetchKeyPair();
53 |
54 | $count = $query->resetGroupBy()
55 | ->resetColumns()
56 | ->select('COUNT(reviews.id) as total_gold_reviews')
57 | ->fetchValue();
58 | ```
--------------------------------------------------------------------------------
/docs/entities.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Working with entities | Sirius ORM
3 | ---
4 |
5 | # Working with entities
6 |
7 | ## Creating new entities
8 |
9 | You can ask the mapper to generate new entities for you using the hydrator.
10 | This will free you from having to build them yourself as it will also construct the proper relations
11 |
12 | ```php
13 | $product = $orm->get('products')
14 | ->newEntity([
15 | 'name' => 'iPhone XXS',
16 | 'sku' => 'ixxs',
17 | 'price'=> 1000.50,
18 | 'category' => [
19 | 'name' => 'Smart phones',
20 | ],
21 | // and on and on...
22 | ]);
23 | ```
24 |
25 | The above code will construct a Category entity and associate it with the Product entity and so on and so forth.
26 |
27 | ## Manipulating the entities
28 |
29 | Once you have an entity from the mapper (from `newEntity` or from a querry) you can manipulate it as described by its interface. In the case of the `GenericEntity` class you can perform things like
30 |
31 | ```php
32 | $product = $orm->find('products', 1);
33 | $product->category->name = 'New category name'; // this works with lazy loading
34 | $product->images->get(0)->path = 'new_image.jpg'; // this too
35 | ```
36 |
37 | One-to-many and Many-to-many relations return Collections, which extend the Doctrine's [ArrayCollection](https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html) so you can do things like
38 |
39 | ```php
40 | if (!$product->images->isEmpty()) {
41 | $product->images->first();
42 | }
43 |
44 | $paths = $product->images->map(function($image) {
45 | return $image->path;
46 | });
47 | ```
48 |
49 | ## Persisting the entities
50 |
51 | ```php
52 | $orm->get('products')->save($product);
53 | ```
54 |
55 | It's that simple! The actions required for persisting an entity are wrapped in a transaction and the ORM will search for all the changes in the "entity tree" and perform the necessary persisting action. You can learn more about the
56 | persistence actions [here](the_actions.md)
57 |
58 | If you don't want for the ORM to look up the "entity tree" and you want to persist only the "root entity" you can do this
59 |
60 | ```php
61 | $orm->get('products')->save($product, false); // it will only persist the product row
62 | ```
63 |
64 | If you want to persist only specific parts of the "entity tree" you do this:
65 |
66 | ```php
67 | $orm->get('products')->save($product, ['category', 'category.parent', 'images']);
68 | ```
69 |
70 | ## Deleting entities
71 |
72 | ```php
73 | $orm->get('products')->delete($product);
74 | ```
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Sirius ORM | Fast yet flexibile ORM built with DX in mind
3 | ---
4 |
5 | # Sirius ORM
6 |
7 | [](https://github.com/siriusphp/orm)
8 | [](https://github.com/siriusphp/orm/releases)
9 | [](https://github.com/siriusphp/orm/blob/master/LICENSE)
10 | [](https://travis-ci.org/siriusphp/orm)
11 | [](https://scrutinizer-ci.com/g/siriusphp/orm/code-structure)
12 | [](https://scrutinizer-ci.com/g/siriusphp/orm)
13 |
14 | Sirius ORM is a [fast and lightweight](https://github.com/adrianmiu/forked-php-orm-benchmark) yet flexible data mapper solution developed with DX in mind. It offers:
15 |
16 | 1. Mapping rows to your own entities
17 | 2. Relations and relation aggregates (COUNT/AVERAGE)
18 | 3. Eager-loading & lazy-loading (without increasing the number of queries)
19 | 4. Queries that let you JOIN with relations (not tables
20 | 5. Deep persistence
21 | 6. Dynamically defined mappers
22 | 7. Speed & low memory usage (no Entity Manager)
23 | 8. 90+% code coverage
24 |
25 | ### Installation
26 |
27 | ```
28 | composer require siriusphp/orm
29 | ```
30 |
31 | ### Initialization
32 |
33 | ```php
34 | use Sirius\Orm\Orm;
35 | use Sirius\Orm\ConnectionLocator;
36 | $connectionLocator = ConnectionLocator::new(
37 | 'mysql:host=localhost;dbname=testdb',
38 | 'username',
39 | 'password'
40 | );
41 | $orm = new Orm($connectionLocator);
42 | ```
43 |
44 | ### Configuration
45 |
46 | AKA, registering mappers and relations
47 |
48 | ```php
49 | $orm->register('pages', MapperConfig::fromArray([
50 | /**
51 | * here goes the configuration
52 | */
53 | ]));
54 |
55 | // continue with the rest of mappers
56 | ```
57 |
58 | ### Usage
59 |
60 | ```php
61 | // find by ID
62 | $page = $orm->find('pages', 1);
63 | // or via the mapper
64 | $page = $orm->get('pages')->find(1);
65 |
66 | // query
67 | $pages = $orm->select('pages')
68 | ->where('status', 'published')
69 | ->orderBy('date desc')
70 | ->limit(10)
71 | ->get();
72 |
73 | // manipulate
74 | $page->title = 'Best ORM evah!';
75 | $page->featured_image->path = 'orm_schema.png';
76 |
77 | // persist
78 | $orm->save($page);
79 | // or via the mapper
80 | $orm->get('pages')->save($page);
81 | ```
82 |
83 | Next: [mappers](mappers.md)
--------------------------------------------------------------------------------
/docs/limits.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Limitations and known issues | Sirius ORM
3 | ---
4 |
5 | # Limits and caveats
6 |
7 | ## 1. No entity manager
8 |
9 | There is no central storage for all the rows extracted from the database to ensure all data is kept in sync.
10 |
11 | If you have circular references and you access the relation a new SQL will be triggered. For example:
12 | - products -> MANY_TO_ONE -> category
13 | - category -> ONE_TO_MANY -> products
14 |
15 | ```php
16 | $product = $orm->find('products', 1);
17 | $product->get('name'); // returns 'old name'
18 | $product->set('name', 'new name');
19 | $productFromCategory = $product->get('category')->get('products')[0];
20 | $productFromCategory->get('name'); // returns 'old name' NOT 'new name'
21 | ```
22 |
23 | I believe this is a reasonable trade-off in the context of the request-response cycle. You usually either retrieve them for display OR modification. Plus, in most cases it is a bad practice do this in the first place.
24 |
25 | ## 2. No database schema utility
26 |
27 | At the moment the _Sirius ORM_ doesn't have a schema generation utility to help with creating migrations
28 |
29 | ## 3. Single database
30 |
31 | At the moment the _Sirius ORM_ doesn't know how to handle relations over multiple databases. The SELECT queries that contain JOINs have to be on the same database.
32 |
33 |
--------------------------------------------------------------------------------
/docs/mappers.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Mappers | Sirius ORM
3 | ---
4 |
5 | # Mappers
6 |
7 | In _Sirius ORM_ Mappers are objects that do the following things:
8 |
9 | - construct [queries](queries.md) for retrieving entities
10 | - generates an executes actions for [persisting](persistence.md) entities
11 |
12 | For this they have to know about a lot of stuff so that the entities remain database-agnostic and as "dumb" as possible: table columns, relations between table columns and entity attributes, the type of entity they handle and the relations between
13 | entities.
14 |
15 | Mappers work together and delegate operations from one to another and they are "registered" within the ORM which acts as a "mapper locator".
16 |
17 | ### 1. Registering mappers via config
18 |
19 | Most of the times you don't need to construct a special `Mapper` class and you can use the one provided by the library. In this case you only need to register with the ORM instance the configuration options like in the example below.
20 |
21 | The exemple includes all config options available although you won't need them all the time (eg: table alias) and some of them have sensible defaults (eg: primary key)
22 |
23 | ```php
24 | use Sirius\Orm\Behaviour\SoftDelete;
25 | use Sirius\Orm\Behaviour\Timestamps;
26 | use Sirius\Orm\MapperConfig;
27 | use Sirius\Orm\Relation\RelationConfig;
28 |
29 | $orm->register('products', MapperConfig::fromArray([
30 | MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
31 | MapperConfig::TABLE => 'tbl_products',
32 | MapperConfig::TABLE_ALIAS => 'products', // if you have tables with prefixes
33 | MapperConfig::PRIMARY_KEY => 'product_id', // defaults to 'id'
34 | MapperConfig::COLUMNS => ['id', 'name', 'price', 'sku'],
35 | MapperConfig::CASTS => ['id' => 'integer', 'price' => 'decimal:2'],
36 | MapperConfig::COLUMN_ATTRIBUTE_MAP => ['sku' => 'code'], // the entity works with the 'code' attribute
37 | MapperConfig::GUARDS => ['published' => 1], // see "The guards" page
38 | MapperConfig::SCOPES => ['sortRandom' => $callback], // see "The query scopes" page
39 | MapperConfig::BEHAVIOURS => [
40 | new SoftDelete('deleted_at'),
41 | new Timestamps(null, 'updated_at')
42 | ],
43 | MapperConfig::RELATIONS => [
44 | 'images' => [
45 | RelationConfig::FOREIGN_MAPPER => 'images'
46 | // see the Relation section for the rest
47 | ]
48 | // rest of the relations go here
49 | ]
50 | ]));
51 | ```
52 |
53 | One advantage of this solution is that the mapper is constructed only when it is requested the first time. If your app doesn't need the `orders` mapper, it won't be constructed.
54 |
55 | ### 2. Registering mappers via factories
56 |
57 | You can use a function when registering a custom mapper with the ORM, like so:
58 |
59 | ```php
60 | $orm->register('products', function($orm) {
61 | // construct the mapper here and return it;
62 | });
63 | ```
64 |
65 | ### 3. Register mapper instances
66 |
67 | ```php
68 | $productsMapper = new ProductMapper($orm);
69 | // make adjustments here to the products mapper
70 | // inject services, add behaviours etc
71 |
72 | $orm->register('products', $productsMapper);
73 | ```
--------------------------------------------------------------------------------
/docs/relation_aggregate.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Relation aggregates | Sirius ORM
3 | ---
4 |
5 | # Relation aggregates
6 |
7 | Sometimes you want to query a relation and extract some aggregates. You may want to count the number of comments on a blog post, the average rating on a product etc. It would be faster if these aggregates are already available somewhere else (a
8 | stats table or in special columns) but sometimes your app doesn't need this type of optimizations.
9 |
10 | The relation aggregates are available on "one to many" and "many to many" relations
11 |
12 | Here's how to work with aggregates
13 |
14 | #### Define the available aggregates for a relation
15 |
16 | ```php
17 | use Sirius\Orm\Relation\RelationConfig;
18 |
19 | $orm->register('products', MapperConfig::make(
20 | // other mapper config goes here
21 | 'relations' => [
22 | 'reviews' => [
23 | 'type' => 'one_to_many',
24 | /**
25 | * other settings go here
26 | */
27 | RelationConfig::AGGREGATES => [
28 | 'reviews_count' => [
29 | RelationConfig::AGG_FUNCTION => 'count(*)',
30 | RelationConfig::AGG_CALLBACK => $functionThatChangesTheQuery
31 | ],
32 | 'average_rating' => [
33 | RelationConfig::AGG_FUNCTION => 'AVERAGE(rating)',
34 | RelationConfig::LOAD_STRATEGY => RelationConfig::LOAD_EAGER, // load the aggregates immediately
35 | ],
36 | ]
37 | ]
38 | ]
39 | ));
40 | ```
41 |
42 | #### Accessing aggregates
43 |
44 | Using lazy loading
45 |
46 | ```php
47 | $products = $orm->select('products')->limit(10)->get();
48 |
49 | foreach ($products as $product) {
50 | echo $product->reviews_count;
51 | echo $product->average_rating;
52 | }
53 | ```
54 |
55 | or eager loading
56 |
57 | ```php
58 | $products = $orm->select('products')
59 | ->load('reviews_count', 'average_rating')
60 | ->limit(10)
61 | ->get();
62 |
63 | foreach ($products as $product) {
64 | echo $product->reviews_count;
65 | echo $product->average_rating;
66 | }
67 | ```
68 |
69 | ## Complex aggregates
70 |
71 | If you need more complex aggregates create queries using [joinWith]
--------------------------------------------------------------------------------
/docs/relation_eav.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Entity-Attribute-Value | Sirius ORM
3 | ---
4 |
5 | # Entity-Attribute-Value relations
6 |
7 | This feature is WIP.
8 |
9 | EAV is a strategy of designing the database so that on one table (the native entity) you store known data about the entity and on another table (the "EAV table") you hold multiple rows each pointing to an attribute and value of the native entity
10 | . The EAV table has one column for the name of the attribute and another with the value.
11 |
12 | If you are not familiar with the EAV concept and you know Wordpress, the `post_meta` table is an EAV table with `meta_name` for the attribute name and `meta_value` as the value.
13 |
14 | It is similar to "one to many" relations with the exception of how the matching rows are attached to the native entity. In the case of the "one to many relations" you attach all matching rows as a collection on the native entity (eg: one product
15 | has one collection of images). In the case of EAV relations each row is identified by a name and it is attached to the native entity as an attribute under that name.
16 |
17 | This allows you to do things like
18 |
19 | ```php
20 | $product->meta_title = "SEO title";
21 | ```
22 |
23 | Given the fact that today's databases support JSON columns you can achieve the same result (ie: hold flexible data) using JSON columns. Still, it's a good feature for an ORM to provide.
24 |
--------------------------------------------------------------------------------
/docs/relation_many_to_many.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Many to many relations | Sirius ORM
3 | ---
4 |
5 | # "Many to many" relations
6 |
7 | Also known as "__belongs to many__" relation in other ORMs.
8 |
9 | Example: many products belong to many tags. This involves a simple pivot table.
10 |
11 | Other ORMs allow you to use another mapper object instead of the pivot table but my experience is that, if the relation is a little more complex, you will end up working with the relations directly.
12 |
13 | For example, many products belong to many orders via the "order items" but, since the "order items" table is complex you will never want to attach/detach products to orders directly, which is something that you would do in the case of a
14 | "many products belong to many tags" kind of situation.
15 |
16 | This ORM is build with a focus on DX and having less options to shoot yourself in the foot is a reasonable trade-off.
17 |
18 | Besides the options explained in the [relations page](relations.html) below you will find those specific to this "many to many" relations:
19 |
20 | > ##### `through_table` / `RelationConfigs::THROUGH_TABLE`
21 |
22 | > - This is the table that links the entities together (products to tags).
23 | > - It defaults to `{plural_of_table_1}_{plural_of_table_2}` where the tables are sorted alphabetically.
24 | A pivot table between `products` and `tags` will be `products_tags` and a pivot between `products` and `categories` would be `categories_products`
25 |
26 | > ##### `through_table_alias` / `RelationConfigs::THROUGH_TABLE_ALIAS`
27 |
28 | > - Not required.
29 | > - The table alias is useful if you are dealing with tables that have prefix. If your DB has a `tbl_products_tags` the alias should be `products_tags`
30 |
31 | > ##### `through_native_column` / `RelationConfigs::THROUGH_NATIVE_COLUMN`
32 |
33 | > - This refers to the column(s) that should match with the native (ie: products) primary key column(s)
34 |
35 | > ##### `through_foreign_column` / `RelationConfigs::THROUGH_FOREIGN_COLUMN`
36 |
37 | > - This refers to the column(s) that should match with the foreign (ie: tags) primary key column(s)
38 |
39 | > ##### `through_guards` / `RelationConfigs::THROUGH_GUARDS`
40 |
41 | > - This refers to guards applied on the through table
42 | > - You can read more about guards [here](the_guards.md).
43 | > - If you are using the same table to link multiple type of entities you can use this option to further filter the queries.
44 | You might have a "content_links" table that holds references between any type of content in your DB (products to tags, users to preferred activities).
45 | > - You might have a column called `native_id` that hold the ID of the product and `foreign_id` that holds the ID of the tag. However, for this set up to work, you would need some guards: `["native_type" => "product", "foreign_type" => "tag"]`
46 | > - The guards are also used when creating/updating rows in the "through table" so you can safely link products and tags together via the global content links table.
47 |
48 | > ##### `through_columns` / `RelationConfigs::THROUGH_COLUMNS`
49 |
50 | > - This option holds the list of columns that are available for modification in the "through table".
51 | > - For example you may link products to tags but also specify a `position` column in the "through table" to let you sort the tags by their position.
52 |
53 | > ##### `through_columns_prefix` / `RelationConfigs::THROUGH_COLUMNS_PREFIX`
54 |
55 | > - This option is for attaching the "through columns" to the tag entity so you may change the.
56 | Defaults to `pivot_`.
57 | > - I don't know about other ORMs but Eloquent forces you to use the `->pivot` property of a foreign entity to do that.
58 | With Sirius Orm you can do just this: `$product->tags[0]->pivot_position = 10;`
59 |
60 | > ##### `aggregates` / `RelationConfig::AGGREGATES`
61 |
62 | > - here you have a list of aggregated values that can be eager/lazy loaded to an entity (count, average, sums)
63 | > - check the [relation aggregates](relation_aggregate.md) page for more details
64 |
65 |
66 | ## Defining a many-to-many relation
67 |
68 | ```php
69 | use Sirius\Orm\Relation\RelationConfig;
70 |
71 | $orm->register('products', MapperConfig::fromArray([
72 | /**
73 | * other mapper config goes here
74 | */
75 | 'relations' => [
76 | 'tags' => [
77 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_MANY,
78 | RelationConfig::FOREIGN_MAPPER => 'tags',
79 | RelationConfig::THROUGH_TABLE => 'tbl_products_to_tags',
80 | RelationConfig::THROUGH_TABLE_ALIAS => 'products_tags',
81 | RelationConfig::NATIVE_KEY => 'product_id',
82 | RelationConfig::FOREIGN_KEY => 'tag_id',
83 | RelationConfig::THROUGH_COLUMNS => ['position', 'created_at'],
84 | RelationConfig::THROUGH_COLUMNS_PREFIX => 'link_',
85 | RelationConfig::QUERY_CALLBACK => function($query) {
86 | $query->orderBy('position DESC');
87 | return $query;
88 | }
89 | ]
90 | ]
91 | ]));
92 | ```
--------------------------------------------------------------------------------
/docs/relation_many_to_one.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Many to one relations | Sirius ORM
3 | ---
4 |
5 | # "Many to one" relations
6 |
7 | Also known as "__belongs to__" relation in other ORMs.
8 |
9 | Examples:
10 | - multiple images belong to one product,
11 | - multiple products belong to a category
12 | - multiple pages belong to one parent.
13 |
14 | There are no special options for this type of relation, besides those explained in the [relations page](relations.html).
15 |
16 | Most of the times (like in the examples above) you don't want to CASCADE delete so this defaults to FALSE.
17 |
18 | ## Definining a many-to-one relation
19 |
20 | ```php
21 | use Sirius\Orm\Relation\RelationConfig;
22 |
23 | $orm->register('products', MapperConfig::make(
24 | // other mapper config goes here
25 | 'relations' => [
26 | 'category' => [
27 | RelationConfig::TYPE => 'many_to_one',
28 | RelationConfig::FOREIGN_MAPPER => 'categories',
29 | RelationConfig::NATIVE_KEY => 'category_id',
30 | RelationConfig::FOREIGN_KEY => 'id',
31 | RelationConfig::QUERY_CALLBACK => function($query) {
32 | $query->orderBy('display_priority DESC');
33 | return $query;
34 | }
35 | ]
36 | ]
37 | ));
38 | ```
--------------------------------------------------------------------------------
/docs/relation_one_to_many.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: One to many relations | Sirius ORM
3 | ---
4 |
5 | # "One to many" relations
6 |
7 | Also known as "__has many__" relation in other ORM.
8 |
9 | Examples:
10 | - one product has many images,
11 | - one category has many products.
12 |
13 | Besides the configuration options explained in the [relations page](relations.html) on "one to many" relation you can have
14 |
15 | > ##### `aggregates` / `RelationConfig::AGGREGATES`
16 |
17 | > - here you have a list of aggregated values that can be eager/lazy loaded to an entity (count, average, sums)
18 | > - check the [relation aggregates](relation_aggregate.md) page for more details
19 |
20 | Most of the times (like in the examples above) you don't want to CASCADE delete so this defaults to FALSE.
21 | One use-case where you want to enable this behaviours is on "one order has many order lines" where you don't need the order lines once the
22 | order is deleted.
23 | But then again, you should let the DB do this.
24 |
25 | ## Defining a one-to-many relation
26 |
27 | In this case the `media` table holds other type of files, not just images
28 |
29 | ```php
30 | use Sirius\Orm\Relation\RelationConfig;
31 |
32 | $orm->register('products', MapperConfig::make(
33 | // other mapper config goes here
34 | 'relations' => [
35 | 'images' => [
36 | RelationConfig::TYPE => 'many_to_one',
37 | RelationConfig::FOREIGN_MAPPER => 'media',
38 | RelationConfig::NATIVE_KEY => 'id',
39 | RelationConfig::FOREIGN_KEY => 'product_id',
40 | // the "media" mapper holds more than images
41 | RelationConfig::FOREIGN_GUARDS => [
42 | 'media_type' => 'image'
43 | ],
44 | // order the images by a specific field
45 | RelationConfig::QUERY_CALLBACK => function($query) {
46 | $query->orderBy('display_priority DESC');
47 | return $query;
48 | }
49 | ]
50 | ]
51 | ));
52 | ```
--------------------------------------------------------------------------------
/docs/relation_one_to_one.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: One to one relations | Sirius ORM
3 | ---
4 |
5 | # "One to one" relations
6 |
7 | Also known as "__has one__" relation in other ORMs.
8 |
9 | On the surface, this relation seems similar to the "many to one". For example you could think that one page has one parent but you would be wrong to classify it as a "one to one" relation.
10 |
11 | The difference is given by the order of operations when saving the table rows. In the "one page has one parent page" scenario you would save first the parent parent page (so you get the ID) and then the child page. This is why it is actually a "many pages have one parent page".
12 |
13 | In a "one to one" relation you first save the "main" entity and then the related entity. One example would be to have a general "content" table and other special tables for each type of content (eg: "content_products", "content_pages") that stores
14 | fields specific to each table.
15 | In this scenario you first save row in the "content" table. Sure, you could have many rows in the "content_products" table for each row in the "content" table (which would make it a "many content_products have one content") but the ORM will only
16 | return the first.
17 |
18 | There are no special options for this type of relation, besides those explained in the [relations page](relations.html).
19 |
20 | In this scenario you would probably want to do CASCADE delete but for this relation the default is still FALSE because usually this happens directly in the database.
21 |
22 |
23 | ## Definining a one-to-one relation
24 |
25 | Here's a typical example
26 |
27 | ```php
28 | use Sirius\Orm\Relation\RelationConfig;
29 |
30 | $orm->register('products', MapperConfig::make(
31 | // other mapper config goes here
32 | 'relations' => [
33 | 'fields' => [
34 | RelationConfig::TYPE => 'one_to_one',
35 | RelationConfig::FOREIGN_MAPPER => 'product_fields',
36 | RelationConfig::NATIVE_KEY => 'id',
37 | RelationConfig::FOREIGN_KEY => 'product_id',
38 | // most likely you want to cascade deletes if the DB doesn't
39 | RelationConfig::CASCADE => true,
40 | // most likely you would want the fields from the start
41 | RelationConfig::LOAD_STRATEGY => RelationConfig::LOAD_EAGER
42 | ]
43 | ]
44 | ));
45 | ```
--------------------------------------------------------------------------------
/docs/relations.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Relations | Sirius ORM
3 | ---
4 |
5 |
6 | # Relations
7 |
8 | Relations have a _name_ (the attribute of the entity that will get the relation), a _native mapper_ (the left-hand side of the relations), a _foreign mapper_ and some options.
9 |
10 | If you pass `MapperConfig` instances to the ORM, which is the easiest way to go, you have to populate the `relations`
11 |
12 | ```php
13 | $orm->register('products', MapperConfig::make(
14 | // other mapper config goes here
15 | 'relations' => [
16 | 'category' => [
17 | 'type' => 'many_to_one',
18 | 'foreign_mapper' => 'categories',
19 | // this is enough if you follow the conventions
20 | ]
21 | ]
22 | ));
23 | ```
24 |
25 | Relations are constructed by the ORM on the first request using the configuration provided.
26 | This means, if a relation is not used, it won't be constructed.
27 |
28 | ## Configuration and conventions
29 |
30 | These are the general configuration options for relations. Check each relation dedicated page for specific additional options and details.
31 |
32 | You can use the `RelationConfig` class constants to make sure you're not making any mistakes when defining the relations
33 |
34 | > ##### `type` / `RelationConfig::TYPE`
35 |
36 | > - holds the type of relation since relations are created on demand
37 | > - supported types: `one_to_one`, `one_to_many`, `many_to_one`, `many_to_many`, `many_to_many_through`, `eav`, `aggregate`
38 |
39 | > ##### `foreign_mapper` / `RelationConfig::FOREIGN_MAPPER`
40 |
41 | > - the name of the mapper as registered in the ORM
42 |
43 | > ##### `native_key` / `RelationConfig::NATIVE_KEY`
44 |
45 | > - is the column(s) in the native mapper that holds the **values** to be searched in the foreign mapper
46 | > - the default value depends on the relation type.
47 | > - for **many to one**: `{foreign mapper table at singular}_{id column of the foreign mapper}` (eg: `category_id` for products -> many to one -> category)
48 | > - for the rest: `{id of the native mapper}` (eg: `id` for category -> one to many -> products)
49 |
50 | > ##### `foreign_key` / `RelationConfig::FOREIGN_KEY`
51 |
52 | > - is the column(s) in the foreign mapper that will be used to query and select the foreign entities
53 | > - the default value depends on the relation type.
54 | > - for **one to many**: `{native mapper table at singular}_{id column of the native mapper}` (eg: `category_id` for category -> one to many -> products)
55 | > - for the rest: `{id of the native mapper}` (eg: `id` for products -> many to one -> category)
56 |
57 | > ##### `foreign_guards` / `RelationConfig::FOREIGN_GUARDS`
58 |
59 | > - learn more about guards [here](the_guards.md)
60 | > - this is useful if the foreign mapper holds multiple types of "content" (eg: a "content" table holds both "pages" and "articles" and uses column "content_type" to determine which is which). The same end goal can be achieved by creating specific
61 | mappers (eg: a "articles" mapper + a "pages" mapper instead of a single "content" mapper)
62 |
63 | > ##### `load_strategy` / `RelationConfig::LOAD_STRATEGY`
64 |
65 | > - by default all relations are loaded lazy since 1) it doesn't affect the number of executed queries and 2) you can specify which relations to be eager loaded on each query
66 | > - however, if you find situations, where you need to have some relations always present it could save you some time
67 | > - you can also set it to `none` if you want for the relation to be loaded ONLY when you specify it (ie: ONLY EAGER)
68 |
69 | > ##### `cascade` / `RelationConfig::CASCADE`
70 |
71 | > - this specifies the behaviour for DELETE operations (ie: also delete related entities)
72 | > - by default the cascade option is `false`
73 |
74 | > ##### `query_callback` / `RelationConfig::QUERY_CALLBACK`
75 |
76 | > - this is for situations where the _foreign guards_ option is not enough you can use a function to executed on the query before retrieving the related entities
77 | > - unlike the _foreign guards_ which are applied on subsequent SAVE operations the _query callback_ is for retrieval only.
78 | You can use this for sorting or limiting the results.
--------------------------------------------------------------------------------
/docs/the_actions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The persistence actions | Sirius ORM
3 | ---
4 |
5 | # Architecture - The persistence actions
6 |
7 | When persisting entity changes (insert/update/delete) Sirius ORM uses `Actions`.
8 |
9 | These are individual classes that responsible for:
10 |
11 | 1. executing the actual necessary queries
12 | 2. updating the entities after successful queries (eg: set the `id` from the `lastInsertId`)
13 | 3. revert changes made to the entities in case of exceptions
14 |
15 | There are actions for delete, insert, update and link entities together (for many-to-many relations).
16 |
17 | **Important!** The Sirius ORM actions are revertable. If there are exceptions while calling
18 | a persistence method (`save` or `delete`) the entities are reverted to their
19 | original state. This way you can use retries in your app if you face connectivity issues.
20 |
21 | One action can also execute other actions since entities are related and, usually, when you want to persist
22 | an entity change you want to also persist the changes to the related entities.
23 |
24 | If you were to save a new (as in "not already in DB") product entity that has a new category
25 | and a set of new tags the operations required would be
26 |
27 | 1. save category entity in the database
28 | 2. set the `category_id` attribute on the product entity
29 | 3. save the product entity in the database
30 | 4. set the `product_id` attribute on each image
31 | 5. save each tag entity in the database
32 | 6. create the links between the product and tags in the pivot table
33 |
34 | The problems Sirius ORM solves with `Action`s are:
35 |
36 | 1. All these operations are wrapped in a transaction and, in case of exceptions the
37 | `revert()` method is called on all the successfully executed actions.
38 | 2. These operations have to be performed in the proper order, dictated by the
39 | relations between the objects.
40 | 3. Behaviours can add their own actions inside the actions tree.
41 |
42 | To solve these problems the Sirius ORM `Action`s are organized in a tree-like
43 | structure that follow these rules:
44 |
45 | 1. Each action executes operations for **only one** entity
46 | (eg: insert the a row in the products table)
47 | 2. Each action may execute other actions before it's own operations
48 | (eg: call the "insert new category" action)
49 | 3. Each action may execute other actions after it's own operations
50 | (eg: call the "insert new image" action)
51 |
52 | The relation between entities determine what type of actions have to be appended
53 | or prepended inside each action and the mapper is does the "heavy lifting" of constructing the
54 | action tree.
55 |
56 | DELETE actions cascade for relations that have this option set to true, otherwise a DELETE action
57 | will also execute SAVE actions for the related entities. However, you should consider letting the DB
58 | handle this.
--------------------------------------------------------------------------------
/docs/the_casting_manager.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The Casting Manager | Sirius ORM
3 | ---
4 |
5 | # Architecture - The Casting Manager
6 |
7 | The Sirius ORM registers for internal usa a **Casting Manager** which is charge of converting and transforming values. It acts as a singleton and is passed around from the ORM instance to mappers and even entities (see `GenericEntity` class)
8 |
9 | The Casting Manager is a registry of functions that receive a value and return a "transformed" value:
10 |
11 | - '1' can be cast as `int` into 1
12 | - '1' can be cast as `bool` into TRUE
13 | - an array can be cast into an entity
14 | - a set of arrays can be cast into a collection of entities
15 |
16 | The Casting Manager is populated with functions by each mapper, functions that delegate to each of the mapper's entity factory to cast arrays as entities or collections of entities
17 |
18 | It is also used by the `GenericEntity` class to ensure the entity's attributes are properly casted.
19 |
20 | You can use for things like creating Carbon dates for some columns:
21 |
22 | ```php
23 | use Carbon\Carbon;
24 |
25 | $orm->getCastingManager()
26 | ->register('date', function($value) {
27 | return Carbon::parse($value);
28 | });
29 | ```
30 |
31 | Since an entity attribute must also be serialized back when persisting you need to define a "cast_for_db" function like so
32 | ```php
33 | use Carbon\Carbon;
34 |
35 | $orm->getCastingManager()
36 | ->register('date_for_db', function($value) {
37 | if ($value instanceof Carbon) {
38 | return $value->format('%Y-%m-%d');
39 | }
40 | return (string) $value;
41 | });
42 | ```
43 |
44 |
45 |
46 | and if you use the `GenericEntity`-based entities
47 |
48 | ```php
49 | use Sirius\Orm\Entity\GenericEntity;
50 |
51 | class Page extends GenericEntity {
52 | protected $casts = ['published_at' => 'date'];
53 | }
54 |
55 |
56 |
57 |
58 | ```
--------------------------------------------------------------------------------
/docs/the_guards.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Entity guards | Sirius ORM
3 | ---
4 |
5 | # Architecture - Entity Guards
6 |
7 | There are many situations where you want to restrict the behaviour of the query used to select entities based on columns that have a specific value AND you also want to enforce those values at the moment the entities are persisted.
8 |
9 | Guards are key-value pairs that correspond to columns that are used while querying or persisting entities and they can be used on Mappers and Relations.
10 |
11 | Example: guard to force the `content_type` column to be equal to `page`
12 |
13 | ```php
14 | use Sirius\Orm\MapperConfig;
15 |
16 | $pageConfig = MapperConfig::fromArray([
17 | MapperConfig::ENTITY_CLASS => 'App\Entity\Page',
18 | MapperConfig::TABLE => 'content',
19 | MapperConfig::GUARDS => ['content_type' => 'page'] //---- HERE
20 | ]);
21 |
22 | $orm->register('pages', $pageConfig);
23 | ```
24 |
25 | This will make all sure that
26 |
27 | 1. the SELECT queries include a `AND content_type="page"` condition
28 | 2. the INSERT\UPDATE quries will include a `SET content_type="page"` instruction
--------------------------------------------------------------------------------
/docs/the_query_scopes.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Query scopes | Sirius ORM
3 | ---
4 |
5 | # Query scopes
6 |
7 | Query scopes are functions that alter the queries and they are used to simplify the code (if they make lots of changes to the query) or express business rules.
8 |
9 | The query scopes are attached to the mapper like so:
10 |
11 | ```php
12 | use Sirius\Orm\MapperConfig;
13 | use Sirius\Orm\Query;
14 |
15 | $orm->register('articles', MapperConfig::fromArray([
16 | /**
17 | * other mapper config goes here
18 | */
19 | MapperConfig::SCOPES => [
20 | 'pickRandomlyFromThosePublished' => function(Query $query, $count = 5) {
21 | $query->where('published', true)
22 | ->orderBy('RAND()')
23 | ->limit($count);
24 | return $query;
25 | },
26 | 'ownedByUser' => function(Query $query, User $user) {
27 | $query->where('author_id', $user->id);
28 | return $query;
29 | }
30 | ]
31 | ]));
32 | ```
33 |
34 | which later can be used like so:
35 |
36 | ```php
37 | $orm->select('articles')
38 | ->ownedByUser($someUser)
39 | ->pickRandomlyFromThosePublished(10);
40 | ```
--------------------------------------------------------------------------------
/docs/the_tracker.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: The tracker | Sirius ORM
3 | ---
4 |
5 | # Architecture - The Tracker object
6 |
7 | The Sirius ORM solves the **n+1 problem** that many ORM face using a `Tracker` object.
8 |
9 | Whenever a mapper is queried for entities, a `Tracker` object stores the rows,
10 | and the relations we might expect to be asked in the future relative to those rows/entities.
11 |
12 | For example, if you query the `Products` mapper for the first 10 products matching some
13 | conditions a `Tracker` object is created that stores those rows, not entities (!!!).
14 |
15 | When the entity is actually created from the row, the ORM builds the list of relations that
16 | the entity might ask in the future (lazy or eager).
17 |
18 | In the case of eager-loaded relations the proper query is executed and the matching entities
19 | are attached to each product.
20 |
21 | In the case of lazy-loaded relations a `LazyValueLoader` is attached to each product as a
22 | placeholder for the related entity. Later, when the consumer asks for the related entity the
23 | `LazyValueLoader` will perform the query and return the matching entities, the entity will
24 | add the result to it's attribute and dispose the `LazyValueLoader`
25 |
--------------------------------------------------------------------------------
/phpunit.mysql.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | ./
15 |
16 |
17 |
18 |
19 | ../src
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ./tests
19 |
20 |
21 |
22 |
23 | ./src
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Action/ActionInterface.php:
--------------------------------------------------------------------------------
1 | nativeMapper = $nativeMapper;
48 | $this->nativeEntity = $nativeEntity;
49 | $this->foreignMapper = $foreignMapper;
50 | $this->foreignEntity = $foreignEntity;
51 | $this->relation = $relation;
52 | $this->actionType = $actionType;
53 | }
54 |
55 | public function revert()
56 | {
57 | /**
58 | * @todo restore previous values
59 | */
60 | }
61 |
62 | public function run()
63 | {
64 | /**
65 | * @todo store current attribute values
66 | */
67 | $this->relation->attachEntities($this->nativeEntity, $this->foreignEntity);
68 | $this->maybeUpdatePivotRow();
69 | }
70 |
71 | public function onSuccess()
72 | {
73 | }
74 |
75 | protected function maybeUpdatePivotRow()
76 | {
77 | if (!$this->relation instanceof ManyToMany) {
78 | return;
79 | }
80 |
81 | $conn = $this->relation->getNativeMapper()->getWriteConnection();
82 | $throughTable = (string)$this->relation->getOption(RelationConfig::THROUGH_TABLE);
83 |
84 | $throughNativeColumns = (array) $this->relation->getOption(RelationConfig::THROUGH_NATIVE_COLUMN);
85 | $throughForeignColumns = (array) $this->relation->getOption(RelationConfig::THROUGH_FOREIGN_COLUMN);
86 | $nativeKey = (array) $this->nativeMapper->getEntityPk($this->nativeEntity);
87 | $foreignKey = (array) $this->foreignMapper->getEntityPk($this->foreignEntity);
88 |
89 | $delete = new \Sirius\Sql\Delete($conn);
90 | $delete->from($throughTable);
91 | foreach ($throughNativeColumns as $k => $col) {
92 | $delete->where($col, $nativeKey[$k]);
93 | $delete->where($throughForeignColumns[$k], $foreignKey[$k]);
94 | }
95 | foreach ((array)$this->relation->getOption(RelationConfig::THROUGH_GUARDS) as $col => $value) {
96 | if (!is_int($col)) {
97 | $delete->where($col, $value);
98 | } else {
99 | $delete->where($value);
100 | }
101 | }
102 | $delete->perform();
103 |
104 | $insertColumns = [];
105 | foreach ($throughNativeColumns as $k => $col) {
106 | $insertColumns[$col] = $nativeKey[$k];
107 | $insertColumns[$throughForeignColumns[$k]] = $foreignKey[$k];
108 | }
109 |
110 | $throughColumnPrefix = $this->relation->getOption(RelationConfig::THROUGH_COLUMNS_PREFIX);
111 | foreach ((array)$this->relation->getOption(RelationConfig::THROUGH_COLUMNS) as $col) {
112 | $insertColumns[$col] = $this->relation
113 | ->getForeignMapper()
114 | ->getEntityAttribute($this->foreignEntity, "{$throughColumnPrefix}{$col}");
115 | }
116 |
117 | foreach ((array)$this->relation->getOption(RelationConfig::THROUGH_GUARDS) as $col => $value) {
118 | if (!is_int($col)) {
119 | $insertColumns[$col] = $value;
120 | }
121 | }
122 |
123 | $insert = new \Sirius\Sql\Insert($conn);
124 | $insert->into($throughTable)
125 | ->columns($insertColumns)
126 | ->perform();
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Action/BaseAction.php:
--------------------------------------------------------------------------------
1 | mapper = $mapper;
64 | $this->entity = $entity;
65 | $this->options = $options;
66 | }
67 |
68 | public function prepend(ActionInterface $action)
69 | {
70 | $this->before[] = $action;
71 | }
72 |
73 | /**
74 | * @return Mapper
75 | */
76 | public function getMapper(): Mapper
77 | {
78 | return $this->mapper;
79 | }
80 |
81 | /**
82 | * @return EntityInterface
83 | */
84 | public function getEntity(): EntityInterface
85 | {
86 | return $this->entity;
87 | }
88 |
89 | public function getOption($name)
90 | {
91 | return $this->options[$name] ?? null;
92 | }
93 |
94 | public function append(ActionInterface $action)
95 | {
96 | $this->after[] = $action;
97 | }
98 |
99 | protected function addActionsForRelatedEntities()
100 | {
101 | if ($this->getOption('relations') === false || ! $this->mapper) {
102 | return;
103 | }
104 |
105 | foreach ($this->getMapper()->getRelations() as $name) {
106 | if (! $this->mapper->hasRelation($name)) {
107 | continue;
108 | }
109 | $this->mapper->getRelation($name)->addActions($this);
110 | }
111 | }
112 |
113 | protected function getConditions()
114 | {
115 | $entityPk = (array)$this->mapper->getPrimaryKey();
116 | $conditions = [];
117 | foreach ($entityPk as $col) {
118 | $val = $this->mapper->getEntityAttribute($this->entity, $col);
119 | if ($val) {
120 | $conditions[$col] = $val;
121 | }
122 | }
123 |
124 | // not enough columns? reset
125 | if (count($conditions) != count($entityPk)) {
126 | return [];
127 | }
128 |
129 | return $conditions;
130 | }
131 |
132 | public function run($calledByAnotherAction = false)
133 | {
134 | $executed = [];
135 |
136 | try {
137 | $this->addActionsForRelatedEntities();
138 |
139 | foreach ($this->before as $action) {
140 | $action->run(true);
141 | $executed[] = $action;
142 | }
143 | $this->execute();
144 | $executed[] = $this;
145 | $this->hasRun = true;
146 | foreach ($this->after as $action) {
147 | $action->run(true);
148 | $executed[] = $action;
149 | }
150 | } catch (\Exception $e) {
151 | $this->undo($executed);
152 | throw new FailedActionException(
153 | sprintf("%s failed for mapper %s", get_class($this), $this->mapper->getTableAlias(true)),
154 | (int)$e->getCode(),
155 | $e
156 | );
157 | }
158 |
159 | /** @var ActionInterface $action */
160 | foreach ($executed as $action) {
161 | // if called by another action, that action will call `onSuccess`
162 | if (! $calledByAnotherAction || $action !== $this) {
163 | $action->onSuccess();
164 | }
165 | }
166 |
167 | return true;
168 | }
169 |
170 | public function revert()
171 | {
172 | return; // each action implements it's own logic if necessary
173 | }
174 |
175 | protected function undo(array $executed)
176 | {
177 | foreach ($executed as $action) {
178 | $action->revert();
179 | }
180 | }
181 |
182 | public function onSuccess()
183 | {
184 | return;
185 | }
186 |
187 |
188 | protected function execute()
189 | {
190 | throw new \BadMethodCallException(sprintf('%s must implement `execute()`', get_class($this)));
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/Action/Delete.php:
--------------------------------------------------------------------------------
1 | getConditions();
13 |
14 | if (empty($conditions)) {
15 | return;
16 | }
17 |
18 | $delete = new \Sirius\Sql\Delete($this->mapper->getWriteConnection());
19 | $delete->from($this->mapper->getTable());
20 | $delete->whereAll($conditions, false);
21 |
22 | $delete->perform();
23 | }
24 |
25 | public function onSuccess()
26 | {
27 | if ($this->entity->getPersistenceState() !== StateEnum::DELETED) {
28 | $this->mapper->setEntityPk($this->entity, null);
29 | $this->entity->setPersistenceState(StateEnum::DELETED);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Action/DeletePivotRows.php:
--------------------------------------------------------------------------------
1 | relation = $relation;
27 | $this->nativeMapper = $relation->getNativeMapper();
28 | $this->nativeEntity = $nativeEntity;
29 | $this->mapper = $relation->getForeignMapper();
30 | $this->entity = $foreignEntity;
31 | }
32 |
33 | protected function execute()
34 | {
35 | $conditions = $this->getConditions();
36 |
37 | if (empty($conditions)) {
38 | return;
39 | }
40 |
41 | $delete = new \Sirius\Sql\Delete($this->mapper->getWriteConnection());
42 | $delete->from((string) $this->relation->getOption(RelationConfig::THROUGH_TABLE));
43 | $delete->whereAll($conditions, false);
44 |
45 | $delete->perform();
46 | }
47 |
48 | public function revert()
49 | {
50 | return; // no change to the entity has actually been performed
51 | }
52 |
53 | public function onSuccess()
54 | {
55 | return;
56 | }
57 |
58 | protected function getConditions()
59 | {
60 | $conditions = [];
61 |
62 | $nativeEntityPk = (array)$this->nativeMapper->getPrimaryKey();
63 | $nativeThroughCols = (array)$this->relation->getOption(RelationConfig::THROUGH_NATIVE_COLUMN);
64 | foreach ($nativeEntityPk as $idx => $col) {
65 | $val = $this->nativeMapper->getEntityAttribute($this->nativeEntity, $col);
66 | if ($val) {
67 | $conditions[$nativeThroughCols[$idx]] = $val;
68 | }
69 | }
70 |
71 | $entityPk = (array)$this->mapper->getPrimaryKey();
72 | $throughCols = (array)$this->relation->getOption(RelationConfig::THROUGH_FOREIGN_COLUMN);
73 | foreach ($entityPk as $idx => $col) {
74 | $val = $this->mapper->getEntityAttribute($this->entity, $col);
75 | if ($val) {
76 | $conditions[$throughCols[$idx]] = $val;
77 | }
78 | }
79 |
80 | // not enough columns? bail
81 | if (count($conditions) != count($entityPk) + count($nativeEntityPk)) {
82 | return [];
83 | }
84 |
85 | return $conditions;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Action/DetachEntities.php:
--------------------------------------------------------------------------------
1 | relation->detachEntities($this->nativeEntity, $this->foreignEntity);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Action/Insert.php:
--------------------------------------------------------------------------------
1 | entityId = $this->mapper->getEntityPk($this->entity);
18 | $this->entityState = $this->entity->getPersistenceState();
19 |
20 | $connection = $this->mapper->getWriteConnection();
21 |
22 | $columns = array_merge(
23 | $this->mapper->extractFromEntity($this->entity),
24 | $this->extraColumns,
25 | $this->mapper->getGuards()
26 | );
27 | $columns = Arr::except($columns, $this->mapper->getPrimaryKey());
28 |
29 | $insertSql = new \Sirius\Sql\Insert($connection);
30 | $insertSql->into($this->mapper->getTable())
31 | ->columns($columns);
32 | $insertSql->perform();
33 | $this->mapper->setEntityPk($this->entity, $connection->lastInsertId());
34 | }
35 |
36 | public function revert()
37 | {
38 | if (! $this->hasRun) {
39 | return;
40 | }
41 | $this->mapper->setEntityPk($this->entity, $this->entityId);
42 | $this->entity->setPersistenceState($this->entityState);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Action/SoftDelete.php:
--------------------------------------------------------------------------------
1 | mapper->getEntityPk($this->entity);
18 | if (! $entityId) {
19 | return;
20 | }
21 |
22 | $this->now = time();
23 |
24 | $update = new \Sirius\Sql\Update($this->mapper->getWriteConnection());
25 | $update->table($this->mapper->getTable())
26 | ->columns([
27 | $this->getOption('deleted_at_column') => $this->now
28 | ])
29 | ->where('id', $entityId);
30 | $update->perform();
31 | }
32 |
33 | public function onSuccess()
34 | {
35 | $this->mapper->setEntityAttribute($this->entity, $this->getOption('deleted_at_column'), $this->now);
36 | if ($this->entity->getPersistenceState() !== StateEnum::DELETED) {
37 | $this->entity->setPersistenceState(StateEnum::DELETED);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Action/Update.php:
--------------------------------------------------------------------------------
1 | $value) {
26 | $this->extraColumns[$name] = $value;
27 | }
28 |
29 | return $this;
30 | }
31 |
32 | protected function execute()
33 | {
34 | $conditions = $this->getConditions();
35 |
36 | if (empty($conditions)) {
37 | return;
38 | }
39 |
40 | $this->entityState = $this->entity->getPersistenceState();
41 |
42 | $connection = $this->mapper->getWriteConnection();
43 |
44 | $columns = $this->mapper->extractFromEntity($this->entity);
45 | $changes = Arr::renameKeys($this->entity->getChanges(), array_flip($this->mapper->getColumnAttributeMap()));
46 | $columns = Arr::only($columns, array_keys($changes));
47 | $columns = array_merge(
48 | $columns,
49 | $this->extraColumns,
50 | $this->mapper->getGuards()
51 | );
52 | $columns = Arr::except($columns, $this->mapper->getPrimaryKey());
53 |
54 | if (count($columns) > 0) {
55 | $updateSql = new \Sirius\Sql\Update($connection);
56 | $updateSql->table($this->mapper->getTable())
57 | ->columns($columns)
58 | ->whereAll($conditions, false);
59 | $updateSql->perform();
60 | }
61 | }
62 |
63 | public function revert()
64 | {
65 | if (! $this->hasRun) {
66 | return;
67 | }
68 | $this->entity->setPersistenceState($this->entityState);
69 | }
70 |
71 | public function onSuccess()
72 | {
73 | foreach ($this->extraColumns as $col => $value) {
74 | $this->mapper->setEntityAttribute($this->entity, $col, $value);
75 | }
76 | $this->entity->setPersistenceState(StateEnum::SYNCHRONIZED);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Behaviour/BehaviourInterface.php:
--------------------------------------------------------------------------------
1 | column = $column;
23 | }
24 |
25 | public function getName()
26 | {
27 | return 'soft_delete';
28 | }
29 |
30 | public function onDelete(Mapper $mapper, Delete $delete)
31 | {
32 | return new \Sirius\Orm\Action\SoftDelete($mapper, $delete->getEntity(), ['deleted_at_column' => $this->column]);
33 | }
34 |
35 | public function onNewQuery(/** @scrutinizer ignore-unused */Mapper $mapper, Query $query)
36 | {
37 | $query->setGuards([
38 | $this->column => null
39 | ]);
40 |
41 | return $query;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Behaviour/Timestamps.php:
--------------------------------------------------------------------------------
1 | createColumn = $createColumn;
26 | $this->updateColumn = $updateColumn;
27 | }
28 |
29 | public function getName()
30 | {
31 | return 'timestamps';
32 | }
33 |
34 | public function onSave(/** @scrutinizer ignore-unused */Mapper $mapper, ActionInterface $action)
35 | {
36 | if ($action instanceof Insert) {
37 | if ($this->createColumn) {
38 | $action->addColumns([$this->createColumn => time()]);
39 | }
40 | if ($this->updateColumn) {
41 | $action->addColumns([$this->updateColumn => time()]);
42 | }
43 | }
44 | if ($action instanceof Update && $this->updateColumn) {
45 | if (! empty($action->getEntity()->getChanges())) {
46 | $action->addColumns([$this->updateColumn => time()]);
47 | }
48 | }
49 |
50 | return $action;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/CastingManager.php:
--------------------------------------------------------------------------------
1 | casts[$name] = $func;
13 | }
14 |
15 | public function cast($type, $value, ...$args)
16 | {
17 | if (strpos($type, ':')) {
18 | list($cast, $args) = explode(':', $type);
19 | $args = explode(',', $args);
20 | } else {
21 | $cast = $type;
22 | }
23 |
24 | if (method_exists($this, $cast)) {
25 | return $this->$cast($value, ...$args);
26 | }
27 |
28 | if (isset($this->casts[$cast])) {
29 | $func = $this->casts[$cast];
30 |
31 | return $func($value, ...$args);
32 | }
33 |
34 | return $value;
35 | }
36 |
37 | public function castArray($arr, $rules)
38 | {
39 | $result = [];
40 |
41 | foreach ($arr as $col => $val) {
42 | if (isset($rules[$col])) {
43 | $result[$col] = $this->cast($rules[$col], $val);
44 | } else {
45 | $result[$col] = $val;
46 | }
47 | }
48 |
49 | return $result;
50 | }
51 |
52 | public function castArrayForDb($arr, $rules)
53 | {
54 | $result = [];
55 |
56 | foreach ($arr as $col => $val) {
57 | if (isset($rules[$col])) {
58 | $result[$col] = $this->cast($rules[$col] . '_for_db', $val);
59 | } else {
60 | $result[$col] = $val;
61 | }
62 | }
63 |
64 | return $result;
65 | }
66 |
67 | public function bool($value)
68 | {
69 | return ! ! $value;
70 | }
71 |
72 | // phpcs:ignore
73 | public function bool_for_db($value)
74 | {
75 | return $value ? 1 : 0;
76 | }
77 |
78 | public function int($value)
79 | {
80 | return $value === null ? null : (int)$value;
81 | }
82 |
83 | public function float($value)
84 | {
85 | return $value === null ? null : (float) $value;
86 | }
87 |
88 | public function decimal($value, $digits)
89 | {
90 | return round((float)$value, (int)$digits);
91 | }
92 |
93 | public function json($value)
94 | {
95 | if (! $value) {
96 | return new \ArrayObject();
97 | }
98 | if (is_array($value)) {
99 | return new \ArrayObject($value);
100 | }
101 | if (is_string($value)) {
102 | return new \ArrayObject(json_decode($value, true));
103 | }
104 | if ($value instanceof \ArrayObject) {
105 | return $value;
106 | }
107 | throw new \InvalidArgumentException('Value has to be a string, an array or an ArrayObject');
108 | }
109 |
110 | // phpcs:ignore
111 | public function json_for_db($value)
112 | {
113 | if (!$value) {
114 | return null;
115 | }
116 | if (is_array($value)) {
117 | return json_encode($value);
118 | }
119 | if ($value instanceof \ArrayObject) {
120 | return json_encode($value->getArrayCopy());
121 | }
122 | return $value;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Collection/Collection.php:
--------------------------------------------------------------------------------
1 | [],
12 | 'added' => []
13 | ];
14 | /**
15 | * @var callable
16 | */
17 | protected $castingFunction;
18 |
19 | public function __construct(array $elements = [], callable $castingFunction = null)
20 | {
21 | parent::__construct($elements);
22 | $this->changes['removed'] = new ArrayCollection();
23 | $this->changes['added'] = new ArrayCollection();
24 | $this->castingFunction = $castingFunction;
25 | }
26 |
27 | protected function castElement($data)
28 | {
29 | $castFunction = $this->castingFunction;
30 | return $castFunction ? call_user_func($castFunction, $data) : $data;
31 | }
32 |
33 | public function add($element)
34 | {
35 | $element = $this->castElement($element);
36 | $this->change('added', $element);
37 |
38 | return parent::add($element);
39 | }
40 |
41 | public function set($key, $value)
42 | {
43 | $value = $this->castElement($value);
44 | parent::set($key, $value);
45 | }
46 |
47 | public function remove($key)
48 | {
49 | $removed = parent::remove($key);
50 | if ($removed) {
51 | $this->change('removed', $removed);
52 | }
53 |
54 | return $removed;
55 | }
56 |
57 | public function removeElement($element)
58 | {
59 | $element = $this->castElement($element);
60 | $removed = parent::removeElement($element);
61 | if ($removed) {
62 | $this->change('removed', $element);
63 | }
64 |
65 | return $removed;
66 | }
67 |
68 | public function getChanges(): array
69 | {
70 | $changes = [];
71 | foreach (array_keys($this->changes) as $t) {
72 | /** @var ArrayCollection $changeCollection */
73 | $changeCollection = $this->changes[$t];
74 | $changes[$t] = $changeCollection->getValues();
75 | }
76 |
77 | return $changes;
78 | }
79 |
80 | public function getArrayCopy()
81 | {
82 | $result = [];
83 | foreach ($this as $element) {
84 | if (is_object($element) && method_exists($element, 'getArrayCopy')) {
85 | $result[] = $element->getArrayCopy();
86 | } else {
87 | $result[] = $element;
88 | }
89 | }
90 | return $result;
91 | }
92 |
93 | protected function change($type, $element)
94 | {
95 | foreach (array_keys($this->changes) as $t) {
96 | /** @var ArrayCollection $changeCollection */
97 | $changeCollection = $this->changes[$t];
98 | if ($t == $type) {
99 | if (! $changeCollection->contains($element)) {
100 | $changeCollection->add($element);
101 | }
102 | } else {
103 | $this->removeElement($element);
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Collection/PaginatedCollection.php:
--------------------------------------------------------------------------------
1 | totalCount = $totalCount;
16 | $this->perPage = $perPage;
17 | $this->currentPage = $currentPage;
18 | }
19 |
20 | /**
21 | * @return mixed
22 | */
23 | public function getTotalCount()
24 | {
25 | return $this->totalCount;
26 | }
27 |
28 | /**
29 | * @return mixed
30 | */
31 | public function getPerPage()
32 | {
33 | return $this->perPage;
34 | }
35 |
36 | /**
37 | * @return mixed
38 | */
39 | public function getCurrentPage()
40 | {
41 | return $this->currentPage;
42 | }
43 |
44 | public function getTotalPages()
45 | {
46 | if ($this->perPage < 1) {
47 | return 0;
48 | }
49 |
50 | return ceil($this->totalCount / $this->perPage);
51 | }
52 |
53 | public function getPageStart()
54 | {
55 | if ($this->totalCount < 1) {
56 | return 0;
57 | }
58 |
59 | return 1 + $this->perPage * ($this->currentPage - 1);
60 | }
61 |
62 | public function getPageEnd()
63 | {
64 | if ($this->totalCount < 1) {
65 | return 0;
66 | }
67 |
68 | return $this->getPageStart() + $this->count() - 1;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | list[$behaviour->getName()] = $behaviour;
17 | }
18 |
19 | public function remove($name)
20 | {
21 | unset($this->list[$name]);
22 | }
23 |
24 | public function without(...$names)
25 | {
26 | $clone = clone $this;
27 | foreach ($names as $name) {
28 | $clone->remove($name);
29 | }
30 | return $clone;
31 | }
32 |
33 | public function apply($mapper, $target, $result, ...$args)
34 | {
35 | foreach ($this->list as $behaviour) {
36 | $method = 'on' . Str::className($target);
37 | if (method_exists($behaviour, $method)) {
38 | $result = $behaviour->{$method}($mapper, $result, ...$args);
39 | }
40 | }
41 |
42 | return $result;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Entity/EntityInterface.php:
--------------------------------------------------------------------------------
1 | castingManager = $castingManager;
31 | foreach ($attributes as $attr => $value) {
32 | $this->set($attr, $value);
33 | }
34 | }
35 |
36 | public function __get($name)
37 | {
38 | return $this->get($name);
39 | }
40 |
41 | public function __set($name, $value)
42 | {
43 | return $this->set($name, $value);
44 | }
45 |
46 | public function __isset($name)
47 | {
48 | return isset($this->attributes[$name]);
49 | }
50 |
51 | public function __unset($name)
52 | {
53 | return $this->set($name, null);
54 | }
55 |
56 | protected function castAttribute($name, $value)
57 | {
58 | $method = Str::methodName($name . ' attribute', 'cast');
59 | if (method_exists($this, $method)) {
60 | return $this->$method($value);
61 | }
62 |
63 | if (!$this->castingManager) {
64 | return $value;
65 | }
66 |
67 | /**
68 | * @todo implement additional attributes
69 | */
70 | $type = $this->casts[$name] ?? $name;
71 |
72 | return $this->castingManager->cast($type, $value);
73 | }
74 |
75 | protected function set($attribute, $value = null)
76 | {
77 | $this->preventChangesIfDeleted();
78 |
79 | if ($value instanceof LazyLoader) {
80 | $this->lazyLoaders[$attribute] = $value;
81 | return $this;
82 | }
83 |
84 | $value = $this->castAttribute($attribute, $value);
85 | if (! isset($this->attributes[$attribute]) || $value != $this->attributes[$attribute]) {
86 | $this->changed[$attribute] = true;
87 | $this->state = StateEnum::CHANGED;
88 | }
89 | $this->attributes[$attribute] = $value;
90 |
91 | return $this;
92 | }
93 |
94 | protected function get($attribute)
95 | {
96 | if (! $attribute) {
97 | return null;
98 | }
99 |
100 | $this->maybeLazyLoad($attribute);
101 |
102 | return $this->attributes[$attribute] ?? null;
103 | }
104 |
105 | public function getPersistenceState()
106 | {
107 | if (! empty($this->changed)) {
108 | }
109 |
110 | return $this->state;
111 | }
112 |
113 | public function setPersistenceState($state)
114 | {
115 | if ($state == StateEnum::SYNCHRONIZED) {
116 | $this->changed = [];
117 | }
118 | $this->state = $state;
119 | }
120 |
121 | public function getArrayCopy()
122 | {
123 | $copy = $this->attributes;
124 | foreach ($copy as $k => $v) {
125 | if (is_object($v) && method_exists($v, 'getArrayCopy')) {
126 | $copy[$k] = $v->getArrayCopy();
127 | }
128 | }
129 | return $copy;
130 | }
131 |
132 | public function getChanges()
133 | {
134 | $changes = $this->changed;
135 | foreach ($this->attributes as $name => $value) {
136 | if (is_object($value) && method_exists($value, 'getChanges')) {
137 | if (! empty($value->getChanges())) {
138 | $changes[$name] = true;
139 | }
140 | }
141 | }
142 |
143 | return $changes;
144 | }
145 |
146 | protected function preventChangesIfDeleted()
147 | {
148 | if ($this->state == StateEnum::DELETED) {
149 | throw new \BadMethodCallException('Entity was deleted, no further changes are allowed');
150 | }
151 | }
152 |
153 | /**
154 | * @param $attribute
155 | */
156 | protected function maybeLazyLoad($attribute): void
157 | {
158 | if (isset($this->lazyLoaders[$attribute])) {
159 | // preserve state
160 | $state = $this->state;
161 | /** @var LazyLoader $lazyLoader */
162 | $lazyLoader = $this->lazyLoaders[$attribute];
163 | $lazyLoader->load();
164 | unset($this->changed[$attribute]);
165 | unset($this->lazyLoaders[$attribute]);
166 | $this->state = $state;
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/Entity/GenericEntityHydrator.php:
--------------------------------------------------------------------------------
1 | mapper = $mapper;
25 | }
26 |
27 | public function setCastingManager(CastingManager $castingManager)
28 | {
29 | $this->castingManager = $castingManager;
30 | }
31 |
32 | public function hydrate(array $attributes = [])
33 | {
34 | $attributes = $this->castingManager
35 | ->castArray($attributes, $this->mapper->getCasts());
36 | $attributes = Arr::renameKeys($attributes, $this->mapper->getColumnAttributeMap());
37 | $class = $this->mapper->getEntityClass() ?? GenericEntity::class;
38 |
39 | return new $class($attributes, $this->castingManager);
40 | }
41 |
42 | public function extract(EntityInterface $entity)
43 | {
44 | $data = Arr::renameKeys(
45 | $entity->getArrayCopy(),
46 | array_flip($this->mapper->getColumnAttributeMap())
47 | );
48 | $data = $this->castingManager
49 | ->castArrayForDb($data, $this->mapper->getCasts());
50 |
51 | return Arr::only($data, $this->mapper->getColumns());
52 | }
53 |
54 | public function get($entity, $attribute)
55 | {
56 | return $entity->{$attribute};
57 | }
58 |
59 | public function set($entity, $attribute, $value)
60 | {
61 | return $entity->{$attribute} = $value;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Entity/HydratorInterface.php:
--------------------------------------------------------------------------------
1 | entity = $entity;
27 | $this->tracker = $tracker;
28 | $this->aggregate = $aggregate;
29 | }
30 |
31 | public function load()
32 | {
33 | $results = $this->tracker->getAggregateResults($this->aggregate);
34 | $this->aggregate->attachAggregateToEntity($this->entity, $results);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Entity/LazyLoader.php:
--------------------------------------------------------------------------------
1 | entity = $entity;
26 | $this->tracker = $tracker;
27 | $this->relation = $relation;
28 | }
29 |
30 | public function load()
31 | {
32 | $results = $this->tracker->getResultsForRelation($this->relation->getOption('name'));
33 | $this->relation->attachMatchesToEntity($this->entity, $results);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Entity/StateEnum.php:
--------------------------------------------------------------------------------
1 | rows = $rows;
44 | }
45 |
46 | public function setRelation($name, Relation $relation, $callback, array $nextLoad = [], $overwrite = false)
47 | {
48 | if ($overwrite || ! isset($this->relations[$name])) {
49 | $this->relations[$name] = $relation;
50 | $this->relationCallback[$name] = $callback;
51 | if (!empty($nextLoad)) {
52 | $this->relationNextLoad[$name] = $nextLoad;
53 | }
54 | }
55 | }
56 |
57 | public function hasRelation($name)
58 | {
59 | return isset($this->relations[$name]);
60 | }
61 |
62 | public function getResultsForRelation($name)
63 | {
64 | if (! isset($this->relations[$name])) {
65 | return [];
66 | }
67 |
68 | if (isset($this->relationResults[$name])) {
69 | return $this->relationResults[$name];
70 | }
71 |
72 | $results = $this->queryRelation($name);
73 |
74 | $this->relationResults[$name] = $results;
75 |
76 | return $this->relationResults[$name];
77 | }
78 |
79 | public function getAggregateResults(Aggregate $aggregate)
80 | {
81 | $name = $aggregate->getName();
82 |
83 | if (isset($this->aggregateResults[$name])) {
84 | return $this->aggregateResults[$name];
85 | }
86 |
87 | /** @var Query $query */
88 | $query = $aggregate->getQuery($this);
89 |
90 | $results = $query->fetchAll();
91 | $this->aggregateResults[$name] = $results instanceof Collection ? $results->getValues() : $results;
92 |
93 | return $this->aggregateResults[$name];
94 | }
95 |
96 | public function pluck($columns)
97 | {
98 | $result = [];
99 | foreach ($this->rows as $row) {
100 | $value = $this->getColumnsFromRow($row, $columns);
101 | if ($value && !in_array($value, $result)) {
102 | $result[] = $value;
103 | }
104 | }
105 |
106 | return $result;
107 | }
108 |
109 | protected function getColumnsFromRow($row, $columns)
110 | {
111 | if (is_array($columns) && count($columns) > 1) {
112 | $result = [];
113 | foreach ($columns as $column) {
114 | if ($row instanceof GenericEntity) {
115 | $result[] = $row->{$column};
116 | } else {
117 | $result[] = $row[$column] ?? null;
118 | }
119 | }
120 |
121 | return $result;
122 | }
123 |
124 | $column = is_array($columns) ? $columns[0] : $columns;
125 |
126 | return $row instanceof GenericEntity ? $row->{$column} : ($row[$column] ?? null);
127 | }
128 |
129 | /**
130 | * After the entities are created we use this method to swap the rows
131 | * with the actual entities to save some memory since rows can be quite big
132 | *
133 | * @param array $entities
134 | */
135 | public function replaceRows(array $entities)
136 | {
137 | $this->rows = $entities;
138 | }
139 |
140 | /**
141 | * @param $name
142 | *
143 | * @return array
144 | */
145 | protected function queryRelation($name)
146 | {
147 | /** @var Relation $relation */
148 | $relation = $this->relations[$name];
149 | /** @var Query $query */
150 | $query = $relation->getQuery($this);
151 |
152 | $queryCallback = $this->relationCallback[$name] ?? null;
153 | if ($queryCallback && is_callable($queryCallback)) {
154 | $query = $queryCallback($query);
155 | }
156 |
157 | $queryNextLoad = $this->relationNextLoad[$name] ?? [];
158 | if ($queryNextLoad && ! empty($queryNextLoad)) {
159 | foreach ($queryNextLoad as $next => $callback) {
160 | $query = $query->load([$next => $callback]);
161 | }
162 | }
163 |
164 | $results = $query->get();
165 | $results = $results instanceof Collection ? $results->getValues() : $results;
166 | $results = $relation->indexQueryResults($results);
167 |
168 | return $results;
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/Exception/FailedActionException.php:
--------------------------------------------------------------------------------
1 | $value) {
14 | if (substr($key, 0, $prefixLen) != $prefix) {
15 | continue;
16 | }
17 | $children[substr($key, $prefixLen)] = $value;
18 | }
19 |
20 | return $children;
21 | }
22 |
23 | public static function ensureParents(array $arr, string $separator = '.'): array
24 | {
25 | foreach ($arr as $key => $value) {
26 | if (strpos($key, $separator)) {
27 | $parents = static::getParents($key, $separator);
28 | foreach ($parents as $parent) {
29 | if (! isset($arr[$parent])) {
30 | $arr[$parent] = null;
31 | }
32 | }
33 | }
34 | }
35 |
36 | return $arr;
37 | }
38 |
39 | protected static function getParents(string $path, string $separator): array
40 | {
41 | $parts = explode($separator, substr($path, 0, strrpos($path, $separator)));
42 | $current = '';
43 | $parents = [];
44 | foreach ($parts as $part) {
45 | $current = $current . ($current ? $separator : '') . $part;
46 | $parents[] = $current;
47 | }
48 |
49 | return $parents;
50 | }
51 |
52 | public static function only(array $arr, array $keys)
53 | {
54 | return array_intersect_key($arr, array_flip((array)$keys));
55 | }
56 |
57 | public static function except(array $arr, $keys)
58 | {
59 | foreach ((array)$keys as $key) {
60 | unset($arr[$key]);
61 | }
62 |
63 | return $arr;
64 | }
65 |
66 | public static function renameKeys(array $arr, array $newNames)
67 | {
68 | if (empty($newNames)) {
69 | return $arr;
70 | }
71 |
72 | foreach (array_keys($newNames) as $name) {
73 | if (isset($arr[$name])) {
74 | $arr[$newNames[$name]] = $arr[$name];
75 | unset($arr[$name]);
76 | }
77 | }
78 |
79 | return $arr;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Helpers/Inflector.php:
--------------------------------------------------------------------------------
1 | $col) {
24 | $parts[] = "{$firsTable}.{$col} = {$secondTable}.{$secondColumns[$k]}";
25 | }
26 |
27 | return implode(' AND ', $parts);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Helpers/Str.php:
--------------------------------------------------------------------------------
1 | [],
10 | 'methodName' => [],
11 | 'methodName' => [],
12 | 'variableName' => [],
13 | 'className' => [],
14 | ];
15 |
16 | public static function underscore($str)
17 | {
18 | if (! isset(static::$cache['underscore'][$str])) {
19 | $str = strtolower($str);
20 | $str = preg_replace("/[^a-z0-9]+/", ' ', $str);
21 |
22 | static::$cache['underscore'][$str] = str_replace(' ', '_', $str);
23 | }
24 |
25 | return static::$cache['underscore'][$str];
26 | }
27 |
28 | public static function methodName($str, $verb)
29 | {
30 | $key = $verb . $str;
31 | if (! isset(static::$cache['methodName'][$key])) {
32 | static::$cache['methodName'][$key] = strtolower($verb) . static::className($str);
33 | }
34 |
35 | return static::$cache['methodName'][$key];
36 | }
37 |
38 | public static function variableName($str)
39 | {
40 | if (! isset(static::$cache['variableName'][$str])) {
41 | $class = static::className($str);
42 |
43 | static::$cache['variableName'][$str] = strtolower(substr($class, 0, 1)) . substr($class, 1);
44 | }
45 |
46 | return static::$cache['variableName'][$str];
47 | }
48 |
49 | public static function className($str)
50 | {
51 | if (! isset(static::$cache['className'][$str])) {
52 | $str = strtolower($str);
53 | $str = preg_replace("/[^a-z0-9]+/", ' ', $str);
54 | $str = ucwords($str);
55 |
56 | static::$cache['className'][$str] = str_replace(' ', '', $str);
57 | }
58 |
59 | return static::$cache['className'][$str];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/MapperConfig.php:
--------------------------------------------------------------------------------
1 | entity attribute)
60 | * @var array
61 | */
62 | public $columnAttributeMap = [];
63 |
64 | /**
65 | * @var null|HydratorInterface
66 | */
67 | public $entityHydrator = null;
68 |
69 | /**
70 | * Default attributes
71 | * @var array
72 | */
73 | public $entityDefaultAttributes = [];
74 |
75 | /**
76 | * List of behaviours to be attached to the mapper
77 | * @var array[BehaviourInterface]
78 | */
79 | public $behaviours = [];
80 |
81 | /**
82 | * List of relations of the configured mapper
83 | * (key = name of relation, value = relation instance)
84 | * @var array[BaseRelation]
85 | */
86 | public $relations = [];
87 |
88 | /**
89 | * List of query callbacks that can be called directly from the query
90 | * @var array
91 | */
92 | public $scopes = [];
93 |
94 | /**
95 | * List of column-value pairs that act as global filters
96 | * @var array
97 | */
98 | public $guards = [];
99 |
100 | public static function fromArray(array $array)
101 | {
102 | $instance = new self;
103 | foreach ($array as $k => $v) {
104 | $instance->{$k} = $v;
105 | }
106 |
107 | return $instance;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Orm.php:
--------------------------------------------------------------------------------
1 | connectionLocator = $connectionLocator;
46 |
47 | if (! $castingManager) {
48 | $castingManager = new CastingManager();
49 | }
50 | $this->castingManager = $castingManager;
51 |
52 | $this->relationBuilder = new RelationBuilder($this);
53 | }
54 |
55 | public function register($name, $mapperOrConfigOrFactory): self
56 | {
57 | if ($mapperOrConfigOrFactory instanceof MapperConfig || is_callable($mapperOrConfigOrFactory)) {
58 | $this->lazyMappers[$name] = $mapperOrConfigOrFactory;
59 | } elseif ($mapperOrConfigOrFactory instanceof Mapper) {
60 | $this->mappers[$name] = $mapperOrConfigOrFactory;
61 | $mapperOrConfigOrFactory->registerCasts($this->castingManager);
62 | } else {
63 | throw new InvalidArgumentException('$mapperOrConfigOrFactory must be a Mapper instance,
64 | a MapperConfig instance or a callable that returns a Mapper instance');
65 | }
66 |
67 | return $this;
68 | }
69 |
70 | public function has($name): bool
71 | {
72 | return isset($this->mappers[$name]) || isset($this->lazyMappers[$name]);
73 | }
74 |
75 | public function get($name): Mapper
76 | {
77 | if (isset($this->lazyMappers[$name])) {
78 | $this->mappers[$name] = $this->buildMapper($this->lazyMappers[$name]);
79 | $this->mappers[$name]->registerCasts($this->castingManager);
80 | unset($this->lazyMappers[$name]);
81 | }
82 |
83 | if (! isset($this->mappers[$name]) || ! $this->mappers[$name]) {
84 | throw new InvalidArgumentException(sprintf('Mapper named %s is not registered', $name));
85 | }
86 |
87 | return $this->mappers[$name];
88 | }
89 |
90 | public function save($mapperName, EntityInterface $entity, $withRelations = true)
91 | {
92 | return $this->get($mapperName)->save($entity, $withRelations);
93 | }
94 |
95 | public function delete($mapperName, EntityInterface $entity, $withRelations = true)
96 | {
97 | return $this->get($mapperName)->delete($entity, $withRelations);
98 | }
99 |
100 | public function find($mapperName, $id, array $load = [])
101 | {
102 | return $this->get($mapperName)->find($id, $load);
103 | }
104 |
105 | public function select($mapperName): Query
106 | {
107 | return $this->get($mapperName)->newQuery();
108 | }
109 |
110 | public function createRelation(Mapper $nativeMapper, $name, $options): Relation
111 | {
112 | return $this->relationBuilder->newRelation($nativeMapper, $name, $options);
113 | }
114 |
115 | private function buildMapper($mapperConfigOrFactory): Mapper
116 | {
117 | if ($mapperConfigOrFactory instanceof MapperConfig) {
118 | return Mapper::make($this, $mapperConfigOrFactory);
119 | }
120 |
121 | $mapper = $mapperConfigOrFactory($this);
122 | if (! $mapper instanceof Mapper) {
123 | throw new InvalidArgumentException(
124 | 'The mapper generated from the factory is not a valid `Mapper` instance'
125 | );
126 | }
127 |
128 | return $mapper;
129 | }
130 |
131 | public function getConnectionLocator(): ConnectionLocator
132 | {
133 | return $this->connectionLocator;
134 | }
135 |
136 | /**
137 | * @return CastingManager
138 | */
139 | public function getCastingManager(): CastingManager
140 | {
141 | return $this->castingManager;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | getReadConnection(), $bindings, $indent);
36 | $this->mapper = $mapper;
37 | $this->from($this->mapper->getTableReference());
38 | $this->resetColumns();
39 | $this->columns($this->mapper->getTableAlias(true) . '.*');
40 | }
41 |
42 | public function __call(string $method, array $params)
43 | {
44 | $scope = $this->mapper->getQueryScope($method);
45 | if ($scope && is_callable($scope)) {
46 | return $scope($this, ...$params);
47 | }
48 |
49 | return parent::__call($method, $params);
50 | }
51 |
52 | public function __clone()
53 | {
54 | $vars = get_object_vars($this);
55 | unset($vars['mapper']);
56 | foreach ($vars as $name => $prop) {
57 | if (is_object($prop)) {
58 | $this->$name = clone $prop;
59 | }
60 | }
61 | }
62 |
63 |
64 | public function load(...$relations): self
65 | {
66 | foreach ($relations as $relation) {
67 | if (is_array($relation)) {
68 | $name = key($relation);
69 | $callback = current($relation);
70 | } else {
71 | $name = $relation;
72 | $callback = null;
73 | }
74 | $this->load[$name] = $callback;
75 | }
76 |
77 | return $this;
78 | }
79 |
80 | public function joinWith($name): Query
81 | {
82 | if (! $this->mapper->hasRelation($name)) {
83 | throw new \InvalidArgumentException(
84 | sprintf("Relation %s, not defined for %s", $name, $this->mapper->getTable())
85 | );
86 | }
87 | $relation = $this->mapper->getRelation($name);
88 |
89 | return $relation->joinSubselect($this, $name);
90 | }
91 |
92 | public function subSelectForJoinWith(): Query
93 | {
94 | $subselect = new Query($this->mapper, $this->bindings, $this->indent . ' ');
95 | $subselect->resetFrom();
96 | $subselect->resetColumns();
97 |
98 | return $subselect;
99 | }
100 |
101 | public function first()
102 | {
103 | $row = $this->fetchOne();
104 |
105 | return $this->mapper->newEntityFromRow($row, $this->load);
106 | }
107 |
108 | public function get(): Collection
109 | {
110 | return $this->mapper->newCollectionFromRows(
111 | $this->connection->fetchAll($this->getStatement(), $this->getBindValues()),
112 | $this->load
113 | );
114 | }
115 |
116 | public function paginate($perPage, $page): PaginatedCollection
117 | {
118 | /** @var Query $countQuery */
119 | $countQuery = clone $this;
120 | $total = $countQuery->count();
121 |
122 | if ($total == 0) {
123 | $this->mapper->newPaginatedCollectionFromRows([], $total, $perPage, $page, $this->load);
124 | }
125 |
126 | $this->perPage($perPage);
127 | $this->page($page);
128 |
129 | return $this->mapper->newPaginatedCollectionFromRows($this->fetchAll(), $total, $perPage, $page, $this->load);
130 | }
131 |
132 | /**
133 | * Executes the query with a limit of $size and applies the callback on each entity
134 | * The callback can change the DB in such a way that you can end up in an infinite loop
135 | * (depending on the sorting) so we set a limit on the number of chunks that can be processed
136 | *
137 | * @param int $size
138 | * @param callable $callback
139 | * @param int $limit
140 | */
141 | public function chunk(int $size, callable $callback, int $limit = 100000)
142 | {
143 | if (!$this->orderBy->build()) {
144 | $this->orderBy(...(array) $this->mapper->getPrimaryKey());
145 | }
146 |
147 | $run = 0;
148 | while ($run < $limit) {
149 | $query = clone $this;
150 | $query->limit($size);
151 | $query->offset($run * $size);
152 |
153 | $results = $query->get();
154 |
155 | if (count($results) === 0) {
156 | break;
157 | }
158 |
159 | foreach ($results as $entity) {
160 | $callback($entity);
161 | }
162 |
163 | $run++;
164 | }
165 | }
166 |
167 | public function count()
168 | {
169 | $this->resetOrderBy();
170 | $this->resetColumns();
171 | $this->columns('COUNT(*) AS total');
172 |
173 | return (int)$this->fetchValue();
174 | }
175 |
176 | public function setGuards(array $guards)
177 | {
178 | foreach ($guards as $column => $value) {
179 | if (is_int($column)) {
180 | $this->guards[] = $value;
181 | } else {
182 | $this->guards[$column] = $value;
183 | }
184 | }
185 |
186 | return $this;
187 | }
188 |
189 | public function resetGuards()
190 | {
191 | $this->guards = [];
192 |
193 | return;
194 | }
195 |
196 | protected function applyGuards()
197 | {
198 | if (empty($this->guards)) {
199 | return;
200 | }
201 |
202 | $this->groupCurrentWhere();
203 | foreach ($this->guards as $column => $value) {
204 | if (is_int($column)) {
205 | $this->where($value);
206 | } else {
207 | $this->where($column, $value);
208 | }
209 | }
210 | }
211 |
212 | public function getStatement(): string
213 | {
214 | $this->applyGuards();
215 |
216 | return parent::getStatement();
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/QueryBuilder.php:
--------------------------------------------------------------------------------
1 | getQueryClass($mapper);
29 |
30 | return new $queryClass($mapper);
31 | }
32 |
33 | protected function getQueryClass(Mapper $mapper)
34 | {
35 | $mapperClass = get_class($mapper);
36 | if (! isset($this->queryClasses[$mapperClass])) {
37 | $queryClass = $mapperClass . 'Query';
38 | if (class_exists($queryClass)) {
39 | $this->queryClasses[$mapperClass] = $queryClass;
40 | } else {
41 | $this->queryClasses[$mapperClass] = Query::class;
42 | }
43 | }
44 |
45 | return $this->queryClasses[$mapperClass];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Relation/Aggregate.php:
--------------------------------------------------------------------------------
1 | name = $name;
30 | $this->relation = $relation;
31 | $this->options = $options;
32 | }
33 |
34 | public function getQuery(Tracker $tracker)
35 | {
36 | $keys = $this->relation->getKeyPairs();
37 |
38 | /** @var Query $query */
39 | $query = $this->relation->getQuery($tracker);
40 | $query->resetColumns();
41 | $query->columns(...array_values($keys));
42 | $query->columns(sprintf(
43 | '%s as %s',
44 | $this->options[RelationConfig::AGG_FUNCTION],
45 | $this->name
46 | ));
47 |
48 | $callback = $this->options[RelationConfig::AGG_CALLBACK] ?? null;
49 | if (is_callable($callback)) {
50 | $query = $callback($query);
51 | }
52 |
53 | $query->groupBy(...array_values($keys));
54 |
55 | return $query;
56 | }
57 |
58 | public function attachLazyAggregateToEntity(EntityInterface $entity, Tracker $tracker)
59 | {
60 | $valueLoader = new LazyAggregate($entity, $tracker, $this);
61 | $this->relation->getNativeMapper()->setEntityAttribute($entity, $this->name, $valueLoader);
62 | }
63 |
64 | public function attachAggregateToEntity(EntityInterface $entity, array $results)
65 | {
66 | $found = null;
67 | foreach ($results as $row) {
68 | if ($this->entityMatchesRow($entity, $row)) {
69 | $found = $row;
70 | break;
71 | }
72 | }
73 | $this->relation
74 | ->getNativeMapper()
75 | ->setEntityAttribute($entity, $this->name, $found ? $found[$this->name] : null);
76 | }
77 |
78 | public function isLazyLoad()
79 | {
80 | return !isset($this->options[RelationConfig::LOAD_STRATEGY]) ||
81 | $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_LAZY;
82 | }
83 |
84 | public function isEagerLoad()
85 | {
86 | return isset($this->options[RelationConfig::LOAD_STRATEGY]) &&
87 | $this->options[RelationConfig::LOAD_STRATEGY] == RelationConfig::LOAD_EAGER;
88 | }
89 |
90 | public function getName()
91 | {
92 | return $this->name;
93 | }
94 |
95 | private function entityMatchesRow(EntityInterface $entity, $row)
96 | {
97 | $keys = $this->relation->getKeyPairs();
98 | foreach ($keys as $nativeCol => $foreignCol) {
99 | $entityValue = $this->relation->getNativeMapper()->getEntityAttribute($entity, $nativeCol);
100 | $rowValue = $row[$foreignCol];
101 | // if both native and foreign key values are present (not unlinked entities) they must be the same
102 | // otherwise we assume that the entities can be linked together
103 | if ($entityValue && $rowValue && $entityValue != $rowValue) {
104 | return false;
105 | }
106 | }
107 |
108 | return true;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Relation/HasAggregates.php:
--------------------------------------------------------------------------------
1 | aggregates)) {
19 | return;
20 | }
21 |
22 | $aggregates = [];
23 | $aggregatesList = $this->getOption(RelationConfig::AGGREGATES);
24 | if (!is_array($aggregatesList) || empty($aggregatesList)) {
25 | $this->aggregates = $aggregates;
26 | return;
27 | }
28 |
29 | foreach ($aggregatesList as $name => $options) {
30 | $agg = new Aggregate($name, /** @scrutinizer ignore-type */ $this, $options);
31 | $aggregates[$name] = $agg;
32 | }
33 |
34 | $this->aggregates = $aggregates;
35 | }
36 |
37 | public function getAggregates()
38 | {
39 | $this->compileAggregates();
40 |
41 | return $this->aggregates;
42 | }
43 |
44 | abstract public function getOption($name);
45 | }
46 |
--------------------------------------------------------------------------------
/src/Relation/ManyToOne.php:
--------------------------------------------------------------------------------
1 | foreignMapper->getPrimaryKey();
18 | if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
19 | $this->options[RelationConfig::FOREIGN_KEY] = $foreignKey;
20 | }
21 |
22 | if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
23 | $nativeKey = $this->getKeyColumn($this->name, $foreignKey);
24 | $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
25 | }
26 | }
27 |
28 | public function joinSubselect(Query $query, string $reference)
29 | {
30 | $subselect = $query->subSelectForJoinWith()
31 | ->from($this->foreignMapper->getTable())
32 | ->columns($this->foreignMapper->getTable() . '.*')
33 | ->as($reference);
34 |
35 | $subselect = $this->applyQueryCallback($subselect);
36 |
37 | $subselect = $this->applyForeignGuards($subselect);
38 |
39 | return $query->join('INNER', $subselect->getStatement(), $this->getJoinOnForSubselect());
40 | }
41 |
42 | public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
43 | {
44 | // no point in linking entities if the native one is deleted
45 | if ($nativeEntity->getPersistenceState() == StateEnum::DELETED) {
46 | return;
47 | }
48 |
49 | $nativeId = $this->getEntityId($this->nativeMapper, $nativeEntity, array_keys($this->keyPairs));
50 |
51 | $found = $result[$nativeId] ?? [];
52 |
53 | $this->attachEntities($nativeEntity, $found[0] ?? null);
54 | }
55 |
56 | /**
57 | * @param EntityInterface $nativeEntity
58 | * @param EntityInterface $foreignEntity
59 | */
60 | public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity = null): void
61 | {
62 | // no point in linking entities if the native one is deleted
63 | if ($nativeEntity->getPersistenceState() == StateEnum::DELETED) {
64 | return;
65 | }
66 |
67 | $nativeKey = (array)$this->getOption(RelationConfig::NATIVE_KEY);
68 | $foreignKey = (array)$this->getOption(RelationConfig::FOREIGN_KEY);
69 |
70 | foreach ($nativeKey as $k => $col) {
71 | $this->nativeMapper->setEntityAttribute(
72 | $nativeEntity,
73 | $col,
74 | $foreignEntity ? $this->foreignMapper->getEntityAttribute($foreignEntity, $foreignKey[$k]) : null
75 | );
76 | }
77 |
78 | $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, $foreignEntity);
79 | }
80 |
81 | public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
82 | {
83 | if ($nativeEntity->getPersistenceState() == StateEnum::DELETED) {
84 | return;
85 | }
86 |
87 | // required for DELETED entities that throw errors if they are changed
88 | $state = $foreignEntity->getPersistenceState();
89 | $foreignEntity->setPersistenceState(StateEnum::SYNCHRONIZED);
90 |
91 | $nativeKey = (array)$this->getOption(RelationConfig::NATIVE_KEY);
92 |
93 | foreach ($nativeKey as $k => $col) {
94 | $this->nativeMapper->setEntityAttribute(
95 | $nativeEntity,
96 | $col,
97 | null
98 | );
99 | }
100 |
101 | $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, null);
102 | $foreignEntity->setPersistenceState($state);
103 | }
104 |
105 | protected function addActionOnDelete(BaseAction $action)
106 | {
107 | // no cascade delete? treat it as a save
108 | if (! $this->isCascade()) {
109 | $this->addActionOnSave($action);
110 | } else {
111 | $foreignEntity = $this->nativeMapper
112 | ->getEntityAttribute($action->getEntity(), $this->name);
113 |
114 | if ($foreignEntity) {
115 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
116 | $deleteAction = $this->foreignMapper
117 | ->newDeleteAction($foreignEntity, ['relations' => $remainingRelations]);
118 | $action->prepend($deleteAction);
119 | $action->prepend($this->newSyncAction($action->getEntity(), $foreignEntity, 'delete'));
120 | }
121 | }
122 | }
123 |
124 | protected function addActionOnSave(BaseAction $action)
125 | {
126 | if (!$this->relationWasChanged($action->getEntity())) {
127 | return;
128 | }
129 | $foreignEntity = $this->nativeMapper->getEntityAttribute($action->getEntity(), $this->name);
130 | if ($foreignEntity) {
131 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
132 | $saveAction = $this->foreignMapper
133 | ->newSaveAction($foreignEntity, ['relations' => $remainingRelations]);
134 | $saveAction->addColumns($this->getExtraColumnsForAction());
135 | $action->prepend($saveAction);
136 | $action->prepend($this->newSyncAction($action->getEntity(), $foreignEntity, 'save'));
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Relation/OneToMany.php:
--------------------------------------------------------------------------------
1 | nativeMapper->getPrimaryKey();
20 | if (! isset($this->options[RelationConfig::NATIVE_KEY])) {
21 | $this->options[RelationConfig::NATIVE_KEY] = $nativeKey;
22 | }
23 |
24 | if (! isset($this->options[RelationConfig::FOREIGN_KEY])) {
25 | $prefix = Inflector::singularize($this->nativeMapper->getTable());
26 | $this->options[RelationConfig::FOREIGN_KEY] = $this->getKeyColumn($prefix, $nativeKey);
27 | }
28 |
29 | parent::applyDefaults();
30 | }
31 |
32 | public function getQuery(Tracker $tracker)
33 | {
34 | $nativeKey = $this->options[RelationConfig::NATIVE_KEY];
35 | $nativePks = $tracker->pluck($nativeKey);
36 |
37 | $query = $this->foreignMapper
38 | ->newQuery()
39 | ->where($this->options[RelationConfig::FOREIGN_KEY], $nativePks);
40 |
41 | $query = $this->applyQueryCallback($query);
42 |
43 | $query = $this->applyForeignGuards($query);
44 |
45 | return $query;
46 | }
47 |
48 | public function joinSubselect(Query $query, string $reference)
49 | {
50 | $subselect = $query->subSelectForJoinWith()
51 | ->columns($this->foreignMapper->getTable() . '.*')
52 | ->from($this->foreignMapper->getTable())
53 | ->as($reference);
54 |
55 | $subselect = $this->applyQueryCallback($subselect);
56 |
57 | $subselect = $this->applyForeignGuards($subselect);
58 |
59 | return $query->join('INNER', $subselect->getStatement(), $this->getJoinOnForSubselect());
60 | }
61 |
62 | public function attachMatchesToEntity(EntityInterface $nativeEntity, array $result)
63 | {
64 | // no point in linking entities if the native one is deleted
65 | if ($nativeEntity->getPersistenceState() == StateEnum::DELETED) {
66 | return;
67 | }
68 |
69 | $nativeId = $this->getEntityId($this->nativeMapper, $nativeEntity, array_keys($this->keyPairs));
70 |
71 | $found = $result[$nativeId] ?? [];
72 |
73 | $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, new Collection($found));
74 | }
75 |
76 | public function attachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
77 | {
78 | foreach ($this->keyPairs as $nativeCol => $foreignCol) {
79 | $nativeKeyValue = $this->nativeMapper->getEntityAttribute($nativeEntity, $nativeCol);
80 | $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, $nativeKeyValue);
81 | }
82 | }
83 |
84 | public function detachEntities(EntityInterface $nativeEntity, EntityInterface $foreignEntity)
85 | {
86 | $state = $foreignEntity->getPersistenceState();
87 | $foreignEntity->setPersistenceState(StateEnum::SYNCHRONIZED);
88 | foreach ($this->keyPairs as $nativeCol => $foreignCol) {
89 | $this->foreignMapper->setEntityAttribute($foreignEntity, $foreignCol, null);
90 | }
91 | $this->foreignMapper->setEntityAttribute($foreignEntity, $this->name, null);
92 | $foreignEntity->setPersistenceState($state);
93 | }
94 |
95 | protected function addActionOnDelete(BaseAction $action)
96 | {
97 | $nativeEntity = $action->getEntity();
98 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
99 |
100 | // no cascade delete? treat as save so we can process the changes
101 | if (! $this->isCascade()) {
102 | $this->addActionOnSave($action);
103 | } else {
104 | // retrieve them again from the DB since the related collection might not have everything
105 | // for example due to a relation query callback
106 | $foreignEntities = $this->getQuery(new Tracker([$nativeEntity->getArrayCopy()]))
107 | ->get();
108 |
109 | foreach ($foreignEntities as $foreignEntity) {
110 | $deleteAction = $this->foreignMapper
111 | ->newDeleteAction($foreignEntity, ['relations' => $remainingRelations]);
112 | $action->append($this->newSyncAction($nativeEntity, $foreignEntity, 'delete'));
113 | $action->append($deleteAction);
114 | }
115 | }
116 | }
117 |
118 | protected function addActionOnSave(BaseAction $action)
119 | {
120 | if (!$this->relationWasChanged($action->getEntity())) {
121 | return;
122 | }
123 |
124 | $nativeEntity = $action->getEntity();
125 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
126 |
127 | /** @var Collection $foreignEntities */
128 | $foreignEntities = $this->nativeMapper->getEntityAttribute($nativeEntity, $this->name);
129 | $changes = $foreignEntities->getChanges();
130 |
131 | // save the entities still in the collection
132 | foreach ($foreignEntities as $foreignEntity) {
133 | if (! empty($foreignEntity->getChanges())) {
134 | $saveAction = $this->foreignMapper
135 | ->newSaveAction($foreignEntity, ['relations' => $remainingRelations]);
136 | $saveAction->addColumns($this->getExtraColumnsForAction());
137 | $action->append($this->newSyncAction($nativeEntity, $foreignEntity, 'save'));
138 | $action->append($saveAction);
139 | }
140 | }
141 |
142 | // save entities that were removed but NOT deleted
143 | foreach ($changes['removed'] as $foreignEntity) {
144 | $saveAction = $this->foreignMapper
145 | ->newSaveAction($foreignEntity, ['relations' => $remainingRelations]);
146 | $saveAction->addColumns($this->getExtraColumnsForAction());
147 | $action->append($this->newSyncAction($nativeEntity, $foreignEntity, 'delete'));
148 | $action->append($saveAction);
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/Relation/OneToOne.php:
--------------------------------------------------------------------------------
1 | getPersistenceState() == StateEnum::DELETED) {
15 | return;
16 | }
17 |
18 | $nativeId = $this->getEntityId($this->nativeMapper, $nativeEntity, array_keys($this->keyPairs));
19 |
20 | $found = $result[$nativeId] ?? [];
21 |
22 | $this->nativeMapper->setEntityAttribute($nativeEntity, $this->name, $found[0] ?? null);
23 | }
24 |
25 | protected function addActionOnDelete(BaseAction $action)
26 | {
27 | // no cascade delete? treat it as a save
28 | if (! $this->isCascade()) {
29 | $this->addActionOnSave($action);
30 | } else {
31 | $foreignEntity = $this->nativeMapper
32 | ->getEntityAttribute($action->getEntity(), $this->name);
33 |
34 | if ($foreignEntity) {
35 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
36 | $deleteAction = $this->foreignMapper
37 | ->newDeleteAction($foreignEntity, ['relations' => $remainingRelations]);
38 | $action->prepend($deleteAction);
39 | $action->append($this->newSyncAction($action->getEntity(), $foreignEntity, 'delete'));
40 | }
41 | }
42 | }
43 |
44 | protected function addActionOnSave(BaseAction $action)
45 | {
46 | if (! $this->relationWasChanged($action->getEntity())) {
47 | return;
48 | }
49 | $foreignEntity = $this->nativeMapper->getEntityAttribute($action->getEntity(), $this->name);
50 | if ($foreignEntity) {
51 | $remainingRelations = $this->getRemainingRelations($action->getOption('relations'));
52 | $saveAction = $this->foreignMapper
53 | ->newSaveAction($foreignEntity, ['relations' => $remainingRelations]);
54 | $saveAction->addColumns($this->getExtraColumnsForAction());
55 | $action->prepend($saveAction);
56 | $action->append($this->newSyncAction($action->getEntity(), $foreignEntity, 'save'));
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Relation/RelationBuilder.php:
--------------------------------------------------------------------------------
1 | orm = $orm;
20 | }
21 |
22 | public function newRelation(Mapper $nativeMapper, $name, $options): Relation
23 | {
24 | $foreignMapper = $options[RelationConfig::FOREIGN_MAPPER];
25 | if ($this->orm->has($foreignMapper)) {
26 | if (! $foreignMapper instanceof Mapper) {
27 | $foreignMapper = $this->orm->get($foreignMapper);
28 | }
29 | }
30 | $type = $options[RelationConfig::TYPE];
31 | $relationClass = __NAMESPACE__ . '\\' . Str::className($type);
32 |
33 | if (! class_exists($relationClass)) {
34 | throw new \InvalidArgumentException("{$relationClass} does not exist");
35 | }
36 |
37 | return new $relationClass($name, $nativeMapper, $foreignMapper, $options);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Relation/RelationConfig.php:
--------------------------------------------------------------------------------
1 | orm->get('products');
17 |
18 | $this->insertRow('content', ['content_type' => 'product', 'title' => 'Product 1']);
19 |
20 | $product = $mapper->find(1);
21 | $this->assertNotNull($product);
22 |
23 | $mapper->delete($product);
24 | $this->assertNull($mapper->find(1));
25 | $this->assertNull($product->id);
26 | $this->assertEquals(StateEnum::DELETED, $product->getPersistenceState());
27 | }
28 |
29 | public function test_entity_is_reverted()
30 | {
31 |
32 | $mapper = Mapper::make($this->orm, MapperConfig::fromArray([
33 | MapperConfig::TABLE => 'content',
34 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
35 | MapperConfig::GUARDS => ['content_type' => 'product'],
36 | MapperConfig::BEHAVIOURS => [new \Sirius\Orm\Tests\Behaviour\FakeThrowsException()]
37 | ]));
38 |
39 |
40 | $this->insertRow('content', ['content_type' => 'product', 'title' => 'Product 1']);
41 |
42 | $product = $mapper->find(1);
43 | $this->assertNotNull($product);
44 |
45 | $this->expectException(FailedActionException::class);
46 | $mapper->delete($product);
47 | $this->assertEquals(1, $product->id);
48 | $this->assertEquals(StateEnum::SYNCHRONIZED, $product->getPersistenceState());
49 | }
50 | }
--------------------------------------------------------------------------------
/tests/Action/FakeThrowsException.php:
--------------------------------------------------------------------------------
1 | orm->get('products');
16 |
17 | $product = $mapper->newEntity(['title' => 'Product 1']);
18 |
19 | $this->assertNull($product->id);
20 |
21 | $mapper->save($product);
22 |
23 | $this->assertNotNull($product->id);
24 |
25 | $product = $mapper->find($product->id);
26 | $this->assertEquals('Product 1', $product->title);
27 | }
28 |
29 | public function test_entity_is_reverted()
30 | {
31 |
32 | $mapper = Mapper::make($this->orm, MapperConfig::fromArray([
33 | MapperConfig::TABLE => 'content',
34 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
35 | MapperConfig::GUARDS => ['content_type' => 'product'],
36 | MapperConfig::BEHAVIOURS => [new \Sirius\Orm\Tests\Behaviour\FakeThrowsException()]
37 | ]));
38 |
39 | $this->expectException(\Exception::class);
40 |
41 | $product = $mapper->newEntity(['title' => 'Product 1']);
42 |
43 | $this->assertNull($product->id);
44 |
45 | $mapper->save($product);
46 |
47 | $this->assertNull($product->id);
48 | }
49 | }
--------------------------------------------------------------------------------
/tests/Action/UpdateTest.php:
--------------------------------------------------------------------------------
1 | orm->get('products');
16 |
17 | $product = $mapper->newEntity(['title' => 'Product 1']);
18 | $mapper->save($product);
19 |
20 | // reload after insert
21 | $product = $mapper->find($product->id);
22 | $product->description = 'Description product 1';
23 | $mapper->save($product);
24 |
25 | // reload after save
26 | $product = $mapper->find($product->id);
27 | $this->assertEquals('Description product 1', $product->description);
28 | $this->assertEquals(StateEnum::SYNCHRONIZED, $product->getPersistenceState());
29 | }
30 |
31 | public function test_entity_is_reverted()
32 | {
33 |
34 | $mapper = Mapper::make($this->orm, MapperConfig::fromArray([
35 | MapperConfig::TABLE => 'content',
36 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
37 | MapperConfig::GUARDS => ['content_type' => 'product'],
38 | MapperConfig::BEHAVIOURS => [new \Sirius\Orm\Tests\Behaviour\FakeThrowsException()]
39 | ]));
40 |
41 | $this->insertRow('content', ['content_type' => 'product', 'title' => 'Product 1']);
42 |
43 | $product = $mapper->find(1);
44 | $product->title = 'Product 2';
45 |
46 | $this->expectException(\Exception::class);
47 | $mapper->save($product);
48 | $this->assertEquals(StateEnum::CHANGED, $product->getPersistenceState());
49 | }
50 |
51 | public function test_column_attribute_map()
52 | {
53 |
54 | $mapper = Mapper::make($this->orm, MapperConfig::fromArray([
55 | MapperConfig::TABLE => 'content',
56 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
57 | MapperConfig::COLUMN_ATTRIBUTE_MAP => ['summary' => 'excerpt'],
58 | MapperConfig::GUARDS => ['content_type' => 'product'],
59 | ]));
60 |
61 | $this->insertRow('content', ['content_type' => 'product', 'title' => 'Product 1', 'summary' => 'Excerpt']);
62 |
63 | $product = $mapper->find(1);
64 | $this->assertEquals('Excerpt', $product->excerpt);
65 |
66 | $product->excerpt = 'New excerpt';
67 |
68 | $mapper->save($product);
69 | $product = $mapper->find(1);
70 | $this->assertEquals('New excerpt', $product->excerpt);
71 | }
72 | }
--------------------------------------------------------------------------------
/tests/BaseTestCase.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
39 | $connectionLocator = ConnectionLocator::new($this->connection);
40 | $this->connectionLocator = $connectionLocator;
41 | $this->orm = new Orm($connectionLocator);
42 | $this->createTables(getenv('DB_ENGINE') ? getenv('DB_ENGINE') : 'generic');
43 | $this->loadMappers();
44 | $connectionLocator->logQueries();
45 | }
46 |
47 | public function createTables($fileName = 'generic')
48 | {
49 | foreach (include(__DIR__ . "/resources/tables/{$fileName}.php") as $sql) {
50 | $this->connection->perform($sql);
51 | }
52 | }
53 |
54 | public function loadMappers()
55 | {
56 | $this->orm->register('images', $this->getMapperConfig('images'));
57 | $this->orm->register('tags', $this->getMapperConfig('tags'));
58 | $this->orm->register('categories', $this->getMapperConfig('categories'));
59 | $this->orm->register('products', $this->getMapperConfig('products'));
60 | $this->orm->register('content_products', $this->getMapperConfig('content_products'));
61 |
62 | }
63 |
64 | public function getMapperConfig($name)
65 | {
66 | return include(__DIR__ . '/resources/mappers/' . $name . '.php');
67 | }
68 |
69 | protected function insertRow($table, $values)
70 | {
71 | $insert = new Insert($this->connection);
72 | $insert->into($table)->columns($values);
73 | $this->connection->perform($insert->getStatement(), $insert->getBindValues());
74 | }
75 |
76 | public function assertExpectedQueries($expected)
77 | {
78 | $this->assertEquals($expected, count($this->connectionLocator->getQueries()));
79 | }
80 |
81 | public function assertRowDeleted($table, ...$conditions)
82 | {
83 | $select = new Select($this->connection);
84 | $row = $select->from($table)
85 | ->where(...$conditions)
86 | ->fetchOne();
87 | $this->assertNull($row);
88 | }
89 |
90 | public function assertRowPresent($table, ...$conditions)
91 | {
92 | $select = new Select($this->connection);
93 | $row = $select->from($table)
94 | ->where(...$conditions)
95 | ->fetchOne();
96 | $this->assertNotNull($row);
97 | }
98 |
99 | protected function insertRows($table, $columns, $rows)
100 | {
101 | foreach ($rows as $row) {
102 | $this->insertRow($table, array_combine($columns, $row));
103 | }
104 | }
105 |
106 | protected function assertSameStatement($expect, $actual)
107 | {
108 | $this->assertSame($this->removeWhiteSpace($expect), $this->removeWhiteSpace($actual));
109 | }
110 |
111 | protected function removeWhiteSpace($str)
112 | {
113 | $str = trim($str);
114 | $str = preg_replace('/^[ \t]*/m', '', $str);
115 | $str = preg_replace('/[ \t]*$/m', '', $str);
116 | $str = preg_replace('/[ ]{2,}/m', ' ', $str);
117 | $str = preg_replace('/[\r\n|\n|\r]+/', ' ', $str);
118 | $str = str_replace('( ', '(', $str);
119 | $str = str_replace(' )', ')', $str);
120 |
121 | return $str;
122 | }
123 | }
--------------------------------------------------------------------------------
/tests/Behaviour/FakeThrowsException.php:
--------------------------------------------------------------------------------
1 | prepend(new \Sirius\Orm\Tests\Action\FakeThrowsException());
23 | return $delete;
24 | }
25 |
26 | public function onSave(Mapper $mapper, ActionInterface $delete)
27 | {
28 | $delete->append(new \Sirius\Orm\Tests\Action\FakeThrowsException());
29 | return $delete;
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/tests/Behaviour/SoftDeleteTest.php:
--------------------------------------------------------------------------------
1 | orm, MapperConfig::fromArray([
16 | MapperConfig::TABLE => 'content',
17 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
18 | MapperConfig::GUARDS => ['content_type' => 'product'],
19 | MapperConfig::BEHAVIOURS => [new SoftDelete()]
20 | ]));
21 |
22 | $this->insertRow('content', ['content_type' => 'product', 'title' => 'Product 1']);
23 |
24 | $this->assertTrue($mapper->delete($mapper->find(1)));
25 | $this->assertRowPresent('content', 'id = 1');
26 |
27 | // check the mapper doesn't find the row
28 | $this->assertNull($mapper->find(1));
29 |
30 | // mapper without the behaviour will find the row
31 | $this->assertNotNull($mapper->without('soft_delete')->find(1));
32 | }
33 | }
--------------------------------------------------------------------------------
/tests/Behaviour/TimestampsTest.php:
--------------------------------------------------------------------------------
1 | mapper = Mapper::make($this->orm, MapperConfig::fromArray([
22 | MapperConfig::TABLE => 'content',
23 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
24 | MapperConfig::GUARDS => ['content_type' => 'product'],
25 | MapperConfig::BEHAVIOURS => [new Timestamps()]
26 | ]));
27 |
28 | $product = $this->mapper->newEntity(['title' => 'Product 1']);
29 |
30 | $this->assertNull($product->created_at);
31 | $this->assertNull($product->updated_at);
32 |
33 | $this->assertTrue($this->mapper->save($product));
34 |
35 | $this->assertNotNull($product->created_at);
36 | $this->assertNotNull($product->updated_at);
37 | }
38 | }
--------------------------------------------------------------------------------
/tests/CastingManagerTest.php:
--------------------------------------------------------------------------------
1 | cm = new CastingManager();
16 | }
17 |
18 | public function test_json()
19 | {
20 | $this->assertSame([], $this->cm->cast('json', '')->getArrayCopy());
21 | $this->assertSame(['ab' => 2], $this->cm->cast('json', '{"ab":2}')->getArrayCopy());
22 | $this->assertSame(['ab' => 2], $this->cm->cast('json', ['ab' => 2])->getArrayCopy());
23 | $this->assertSame(['ab' => 2], $this->cm->cast('json', new \ArrayObject(['ab' => 2]))->getArrayCopy());
24 | }
25 |
26 | public function test_json_for_db()
27 | {
28 | $this->assertSame(null, $this->cm->cast('json_for_db', []));
29 | $this->assertSame('{"ab":2}', $this->cm->cast('json_for_db', '{"ab":2}'));
30 | $this->assertSame('{"ab":2}', $this->cm->cast('json_for_db', ['ab' => 2]));
31 | $this->assertSame('{"ab":2}', $this->cm->cast('json_for_db', new \ArrayObject(['ab' => 2])));
32 | }
33 |
34 | public function test_cast_array()
35 | {
36 |
37 | $result = $this->cm->castArray([
38 | 'price' => '10',
39 | 'active' => '1',
40 | ], [
41 | 'price' => 'float',
42 | 'active' => 'bool',
43 | ]);
44 |
45 | $this->assertSame([
46 | 'price' => 10.0,
47 | 'active' => true
48 | ], $result);
49 | }
50 | public function test_cast_array_for_db()
51 | {
52 |
53 | $result = $this->cm->castArrayForDb([
54 | 'price' => 10.0,
55 | 'active' => true
56 | ], [
57 | 'price' => 'float',
58 | 'active' => 'bool',
59 | ]);
60 |
61 | $this->assertSame([
62 | 'price' => 10.0,
63 | 'active' => 1
64 | ], $result);
65 | }
66 | }
--------------------------------------------------------------------------------
/tests/Entity/FakeEntity.php:
--------------------------------------------------------------------------------
1 | 'int',
14 | 'value' => 'decimal:2'
15 | ];
16 |
17 | protected function castFeaturedImageIdAttribute($value)
18 | {
19 | return (int)$value;
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/tests/Helpers/ArrTest.php:
--------------------------------------------------------------------------------
1 | null,
17 | 'key_1.key_3' => null,
18 | 'key_2.key_3' => null,
19 | 'key_1.key_2.key_3' => null
20 | ];
21 |
22 | $this->assertSame([
23 | 'key_2' => null,
24 | 'key_3' => null,
25 | 'key_2.key_3' => null,
26 | ], Arr::getChildren($arr, 'key_1'));
27 |
28 | $this->assertSame([
29 | 'key_3' => null,
30 | ], Arr::getChildren($arr, 'key_1.key_2'));
31 | }
32 |
33 | public function test_ensure_parents()
34 | {
35 | $arr = [
36 | 'key_1.key_2.key_3' => null
37 | ];
38 |
39 | $this->assertSame([
40 | 'key_1.key_2.key_3' => null,
41 | 'key_1' => null,
42 | 'key_1.key_2' => null
43 | ], Arr::ensureParents($arr));
44 | }
45 | }
--------------------------------------------------------------------------------
/tests/Helpers/StrTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('abc_def', Str::underscore('abc Def'));
16 | $this->assertEquals('abc_def', Str::underscore('abc-def'));
17 | $this->assertEquals('abcdef', Str::underscore('abcDef'));
18 | }
19 |
20 | public function test_method_name()
21 | {
22 | $this->assertEquals('getAttributeName', Str::methodName('attribute_name', 'get'));
23 | }
24 |
25 | public function test_variable_name()
26 | {
27 | $this->assertEquals('primaryKey', Str::variableName('primary_key'));
28 | }
29 | }
--------------------------------------------------------------------------------
/tests/MapperTest.php:
--------------------------------------------------------------------------------
1 | mapper = Mapper::make($this->orm, MapperConfig::fromArray([
22 | MapperConfig::TABLE => 'products',
23 | MapperConfig::ENTITY_CLASS => ProductEntity::class,
24 | MapperConfig::TABLE_ALIAS => 'p',
25 | MapperConfig::COLUMNS => ['id', 'category_id', 'featured_image_id', 'sku', 'price'],
26 | MapperConfig::COLUMN_ATTRIBUTE_MAP => ['price' => 'value']
27 | ]));
28 | }
29 |
30 | public function test_new_entity()
31 | {
32 | $product = $this->mapper->newEntity([
33 | 'category_id' => '10',
34 | 'featured_image_id' => '20',
35 | 'sku' => 'sku 1',
36 | 'price' => '100.343'
37 | ]);
38 |
39 | $this->assertEquals(100.34, $product->value);
40 | $this->assertEquals(10, $product->category_id);
41 | $this->assertEquals(20, $product->featured_image_id);
42 | }
43 | }
--------------------------------------------------------------------------------
/tests/OrmTest.php:
--------------------------------------------------------------------------------
1 | 'products',
17 | MapperConfig::TABLE_ALIAS => 'p',
18 | MapperConfig::COLUMNS => ['id', 'category_id', 'featured_image_id', 'sku', 'price']
19 | ]);
20 | $this->orm->register('products', $mapperConfig);
21 |
22 | $this->assertTrue($this->orm->has('products'));
23 | $this->assertInstanceOf(Mapper::class, $this->orm->get('products'));
24 | }
25 |
26 | public function test_lazy_mapper_factory()
27 | {
28 | $mapperConfig = MapperConfig::fromArray([
29 | MapperConfig::TABLE => 'products',
30 | MapperConfig::TABLE_ALIAS => 'p',
31 | MapperConfig::COLUMNS => ['id', 'category_id', 'featured_image_id', 'sku', 'price']
32 | ]);
33 | $this->orm->register('products', function ($orm) use ($mapperConfig) {
34 | return Mapper::make($orm, $mapperConfig);
35 | });
36 |
37 | $this->assertTrue($this->orm->has('products'));
38 | $this->assertInstanceOf(Mapper::class, $this->orm->get('products'));
39 | }
40 |
41 | public function test_mapper_instance()
42 | {
43 | $mapperConfig = MapperConfig::fromArray([
44 | MapperConfig::TABLE => 'products',
45 | MapperConfig::TABLE_ALIAS => 'p',
46 | MapperConfig::COLUMNS => ['id', 'category_id', 'featured_image_id', 'sku', 'price']
47 | ]);
48 | $mapper = Mapper::make($this->orm, $mapperConfig);
49 | $this->orm->register('products', $mapper);
50 |
51 | $this->assertInstanceOf(Mapper::class, $this->orm->get('products'));
52 | }
53 |
54 | public function test_exception_thrown_on_invalid_mapper_instance()
55 | {
56 | $this->expectException(\InvalidArgumentException::class);
57 | $this->orm->register('products', new \stdClass());
58 | }
59 |
60 | public function test_exception_thrown_on_unknown_mapper()
61 | {
62 | $this->expectException(\InvalidArgumentException::class);
63 | $this->orm->get('pages');
64 | }
65 |
66 | public function test_exception_thrown_on_invalid_mapper_factory()
67 | {
68 | $this->expectException(\InvalidArgumentException::class);
69 | $this->orm->register('products', function () {
70 | return new \stdClass();
71 | });
72 | $this->orm->get('products');
73 | }
74 | }
--------------------------------------------------------------------------------
/tests/QueryTest.php:
--------------------------------------------------------------------------------
1 | mapper = $this->orm->get('products');
22 | }
23 |
24 | public function test_find()
25 | {
26 | $this->insertRow('content', [
27 | 'content_type' => 'product',
28 | 'title' => 'Product 1'
29 | ]);
30 | $entity = $this->mapper->find(1);
31 | $this->assertSame('Product 1', $entity->title);
32 |
33 | $this->assertNull($this->mapper->find(2));
34 | }
35 |
36 | public function test_query_get()
37 | {
38 | $this->insertRows('content', ['content_type', 'title'], [
39 | ['product', 'Product 1'],
40 | ['product', 'Product 2'],
41 | ['product', 'Product 3'],
42 | ['product', 'Product 4'],
43 | ]);
44 |
45 | $result = $this->mapper->newQuery()
46 | ->where('title', 'Product 2', '>=')
47 | ->get();
48 |
49 | $this->assertEquals(3, count($result));
50 | }
51 |
52 | public function test_chunk()
53 | {
54 | $this->insertRows('content', ['content_type', 'title'], [
55 | ['product', 'Product 1'],
56 | ['product', 'Product 2'],
57 | ['product', 'Product 3'],
58 | ['product', 'Product 4'],
59 | ['product', 'Product 5'],
60 | ['product', 'Product 6'],
61 | ]);
62 |
63 | $found = 0;
64 | $result = $this->mapper->newQuery()
65 | ->chunk(2, function($entity) use (&$found) {
66 | $found += 1;
67 | }, 2);
68 |
69 | $this->assertEquals(4, $found);
70 | }
71 |
72 | public function test_chunk_no_limit()
73 | {
74 | $this->insertRows('content', ['content_type', 'title'], [
75 | ['product', 'Product 1'],
76 | ['product', 'Product 2'],
77 | ['product', 'Product 3'],
78 | ['product', 'Product 4'],
79 | ['product', 'Product 5'],
80 | ['product', 'Product 6'],
81 | ]);
82 |
83 | $found = 0;
84 | $result = $this->mapper->newQuery()
85 | ->chunk(2, function($entity) use (&$found) {
86 | $found += 1;
87 | });
88 |
89 | $this->assertEquals(6, $found);
90 | }
91 |
92 | public function test_query_paginate()
93 | {
94 | $this->insertRows('content', ['content_type', 'title'], [
95 | ['product', 'Product 1'],
96 | ['product', 'Product 2'],
97 | ['product', 'Product 3'],
98 | ['product', 'Product 4'],
99 | ]);
100 |
101 | $result = $this->mapper->newQuery()
102 | ->paginate(3, 2);
103 |
104 | $this->assertEquals(1, count($result));
105 | $this->assertEquals(2, $result->getCurrentPage());
106 | $this->assertEquals(4, $result->getPageStart());
107 | $this->assertEquals(4, $result->getPageStart());
108 | $this->assertEquals(4, $result->getPageEnd());
109 | $this->assertEquals(2, $result->getTotalPages());
110 | $this->assertEquals(3, $result->getPerPage());
111 | $this->assertEquals(4, $result->getTotalCount());
112 |
113 |
114 | $result = $this->mapper->newQuery()
115 | ->where('title', 'Product 5', '>')
116 | ->paginate(3, 2);
117 |
118 | $this->assertEquals(0, $result->getTotalCount());
119 | $this->assertEquals(0, $result->getPageStart());
120 | $this->assertEquals(0, $result->getPageEnd());
121 | }
122 | }
--------------------------------------------------------------------------------
/tests/Relation/OneToOneTest.php:
--------------------------------------------------------------------------------
1 | loadMappers();
26 |
27 | $this->nativeMapper = $this->orm->get('products');
28 | $this->foreignMapper = $this->orm->get('content_products');
29 | }
30 |
31 | public function test_delete_with_cascade_true()
32 | {
33 | // reconfigure products-featured_image to use CASCADE
34 | $config = $this->getMapperConfig('products');
35 | $config->relations['fields'][RelationConfig::CASCADE] = true;
36 | $this->nativeMapper = $this->orm->register('products', $config)->get('products');
37 |
38 | $this->insertRow('content', ['id' => 1, 'content_type' => 'product', 'title' => 'Product 1']);
39 | $this->insertRow('content_products', ['content_id' => 1, 'featured_image_id' => 2]);
40 |
41 | $product = $this->nativeMapper->find(1);
42 | $this->assertNotNull($product->fields);
43 | $this->assertTrue($this->nativeMapper->delete($product, true));
44 | $this->assertRowDeleted('content', 'id', 1);
45 | $this->assertRowDeleted('content_products', 'content_id', 1);
46 | }
47 |
48 | public function test_delete_with_cascade_false()
49 | {
50 | $this->insertRow('content', ['id' => 1, 'content_type' => 'product', 'title' => 'Product 1']);
51 | $this->insertRow('content_products', ['content_id' => 1, 'featured_image_id' => 2]);
52 |
53 | $product = $this->nativeMapper->find(1);
54 | $product->fields->featured_image_id = 3;
55 |
56 | $this->assertTrue($this->nativeMapper->delete($product, false));
57 | $this->assertRowDeleted('content', 'id', 1);
58 | $fields = $this->foreignMapper->find(1);
59 | $this->assertEquals('2', $fields->featured_image_id);
60 | }
61 |
62 | public function test_save_with_relations()
63 | {
64 | $this->insertRow('content', ['id' => 1, 'content_type' => 'product', 'title' => 'Product 1']);
65 | $this->insertRow('content_products', ['content_id' => 1, 'featured_image_id' => 2]);
66 |
67 | $product = $this->nativeMapper->find(1);
68 | $product->title = 'Product 2';
69 | $product->fields->featured_image_id = 3;
70 |
71 | $this->assertTrue($this->nativeMapper->save($product));
72 |
73 | $product = $this->nativeMapper->find(1);
74 | $this->assertEquals('Product 2', $product->title);
75 | $this->assertEquals(3, $product->fields->featured_image_id);
76 | }
77 |
78 | public function test_save_without_relations()
79 | {
80 | $this->insertRow('content', ['id' => 1, 'content_type' => 'product', 'title' => 'Product 1']);
81 | $this->insertRow('content_products', ['content_id' => 1, 'featured_image_id' => 2]);
82 |
83 | $product = $this->nativeMapper->find(1);
84 | $product->title = 'Product 2';
85 | $product->fields->featured_image_id = 3;
86 |
87 | $this->assertTrue($this->nativeMapper->save($product, false));
88 |
89 | $product = $this->nativeMapper->find(1);
90 | $this->assertEquals('Product 2', $product->title);
91 | $this->assertEquals(2, $product->fields->featured_image_id);
92 | }
93 |
94 | public function test_join_with() {
95 | $query = $this->nativeMapper->newQuery()
96 | ->joinWith('fields');
97 |
98 | // the featured_image is not a real one-to-one relation
99 | $expectedStatement = <<assertSameStatement($expectedStatement, $query->getStatement());
113 | }
114 | }
--------------------------------------------------------------------------------
/tests/Relation/RelationTest.php:
--------------------------------------------------------------------------------
1 | nativeMapper = Mapper::make($this->orm, MapperConfig::fromArray([
19 | MapperConfig::TABLE => 'products',
20 | MapperConfig::COLUMNS => ['id', 'related_col_1', 'related_col_2']
21 | ]));
22 |
23 | $this->foreignMapper = Mapper::make($this->orm, MapperConfig::fromArray([
24 | MapperConfig::TABLE => 'categories',
25 | MapperConfig::PRIMARY_KEY => ['col_1', 'col_2'],
26 | MapperConfig::COLUMNS => ['col_1', 'col_2', 'name']
27 | ]));
28 |
29 | $relation = new ManyToOne('related', $this->nativeMapper, $this->foreignMapper);
30 |
31 | $this->assertSame(['related_col_1', 'related_col_2'], $relation->getOption(RelationConfig::NATIVE_KEY));
32 |
33 | $native1 = new GenericEntity(['related_col_1' => 10, 'related_col_2' => 10]);
34 | $native2 = new GenericEntity(['related_col_1' => 10, 'related_col_2' => 20]);
35 |
36 | $foreign1 = new GenericEntity(['col_1' => 10, 'col_2' => 10]);
37 | $foreign2 = new GenericEntity(['col_1' => 10, 'col_2' => 20]);
38 |
39 | $tracker = new Tracker([
40 | $native1->getArrayCopy(),
41 | $native2->getArrayCopy(),
42 | ]);
43 |
44 | $expectedStatement = <<assertSameStatement($expectedStatement, $relation->getQuery($tracker)->getStatement());
63 | $this->assertSame([
64 | '__1__' => [10, \PDO::PARAM_INT],
65 | '__2__' => [10, \PDO::PARAM_INT],
66 | '__3__' => [10, \PDO::PARAM_INT],
67 | '__4__' => [20, \PDO::PARAM_INT],
68 | ], $relation->getQuery($tracker)->getBindValues());
69 | }
70 | }
--------------------------------------------------------------------------------
/tests/benchmark/AbstractTestSuite.php:
--------------------------------------------------------------------------------
1 | con->exec('DROP TABLE [products]');
46 | $this->con->exec('DROP TABLE [products_tags]');
47 | $this->con->exec('DROP TABLE [tags]');
48 | $this->con->exec('DROP TABLE [categories]');
49 | $this->con->exec('DROP TABLE [images]');
50 | } catch (PDOException $e) {
51 | // do nothing - the tables probably don't exist yet
52 | }
53 | $this->con->exec('CREATE TABLE [products]
54 | (
55 | [id] INTEGER NOT NULL PRIMARY KEY,
56 | [name] VARCHAR(255) NOT NULL,
57 | [sku] VARCHAR(24) NOT NULL,
58 | [price] FLOAT,
59 | [category_id] INTEGER,
60 | FOREIGN KEY (category_id) REFERENCES categories(id)
61 | )');
62 | $this->con->exec('CREATE TABLE [categories]
63 | (
64 | [id] INTEGER NOT NULL PRIMARY KEY,
65 | [name] VARCHAR(128) NOT NULL
66 | )');
67 | $this->con->exec('CREATE TABLE [images]
68 | (
69 | [id] INTEGER NOT NULL PRIMARY KEY,
70 | [imageable_id] INTEGER,
71 | [imageable_type] VARCHAR(128),
72 | [path] VARCHAR(128) NOT NULL
73 | )');
74 | $this->con->exec('CREATE TABLE [tags]
75 | (
76 | [id] INTEGER NOT NULL PRIMARY KEY,
77 | [name] VARCHAR(128) NOT NULL
78 | )');
79 | $this->con->exec('CREATE TABLE [products_tags]
80 | (
81 | [id] INTEGER NOT NULL PRIMARY KEY,
82 | [product_id] INTEGER,
83 | [tag_id] INTEGER,
84 | [position] INTEGER
85 | )');
86 | }
87 |
88 | public function run()
89 | {
90 | $t1 = $this->runMethod('insert');
91 | $t2 = $this->runMethod('update');
92 | $t3 = $this->runMethod('find');
93 | $t4 = $this->runMethod('complexQuery');
94 | $t5 = $this->runMethod('relations');
95 | echo sprintf("| %32s | %6d | %6d | %6d | %6d | %6d |", str_replace('TestSuite', '', get_class($this)), $t1, $t2, $t3, $t4, $t5);
96 | }
97 |
98 | public function runMethod($methodName, $nbTest = self::NB_TEST)
99 | {
100 | // prepare method are used to isolate some operations outside the tests
101 | // for the getters test we isolate the retrieval of the object from the db
102 | $prepareMethod = 'prepare_' . $methodName;
103 | if (method_exists($this, $prepareMethod)) {
104 | $this->$prepareMethod();
105 | }
106 |
107 | $testMethod = 'test_' . $methodName;
108 | $this->$testMethod();
109 |
110 | $timer = new sfTimer();
111 |
112 | $this->clearCache();
113 |
114 | for ($i = 0; $i < $nbTest; $i++) {
115 | $this->$methodName($i);
116 | }
117 | $t = $timer->getElapsedTime();
118 |
119 | return $t * 1000;
120 | }
121 |
122 | public function assertEquals($expected, $actual, $message = null) {
123 | if ($expected != $actual) {
124 | throw new Exception($message ?? sprintf('%s is not the same %s', $expected, $actual));
125 | }
126 | }
127 |
128 | public function assertNotNull($actual, $message = null) {
129 | if (null == $actual) {
130 | throw new Exception($message ?? sprintf('%s is null', $actual));
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/benchmark/TestRunner.php:
--------------------------------------------------------------------------------
1 | initialize();
9 | $test->run();
10 | echo sprintf(" %11s | %6.2f |\n", number_format(memory_get_usage(true) - $memory), (microtime(true) - $time));
11 |
--------------------------------------------------------------------------------
/tests/benchmark/sfTimer.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | /**
12 | * sfTimer class allows to time some PHP code.
13 | *
14 | * @package symfony
15 | * @subpackage util
16 | * @author Fabien Potencier
17 | * @version SVN: $Id: sfTimer.class.php 9079 2008-05-20 00:38:07Z Carl.Vondrick $
18 | */
19 | class sfTimer
20 | {
21 | protected
22 | $startTime = null,
23 | $totalTime = null,
24 | $name = '',
25 | $calls = 0;
26 |
27 | /**
28 | * Creates a new sfTimer instance.
29 | *
30 | * @param string $name The name of the timer
31 | */
32 | public function __construct($name = '')
33 | {
34 | $this->name = $name;
35 | $this->startTimer();
36 | }
37 |
38 | /**
39 | * Starts the timer.
40 | */
41 | public function startTimer()
42 | {
43 | $this->startTime = microtime(true);
44 | }
45 |
46 | /**
47 | * Stops the timer and add the amount of time since the start to the total time.
48 | *
49 | * @return float Time spend for the last call
50 | */
51 | public function addTime()
52 | {
53 | $spend = microtime(true) - $this->startTime;
54 | $this->totalTime += $spend;
55 | ++$this->calls;
56 |
57 | return $spend;
58 | }
59 |
60 | /**
61 | * Gets the number of calls this timer has been called to time code.
62 | *
63 | * @return integer Number of calls
64 | */
65 | public function getCalls()
66 | {
67 | return $this->calls;
68 | }
69 |
70 | /**
71 | * Gets the total time elapsed for all calls of this timer.
72 | *
73 | * @return float Time in seconds
74 | */
75 | public function getElapsedTime()
76 | {
77 | if (null === $this->totalTime)
78 | {
79 | $this->addTime();
80 | }
81 |
82 | return $this->totalTime;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | 'categories',
8 | MapperConfig::COLUMNS => ['id', 'parent_id', 'name'],
9 | MapperConfig::CASTS => [
10 | 'id' => 'int',
11 | 'details' => 'json'
12 | ],
13 | MapperConfig::RELATIONS => [
14 | 'products' => [
15 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
16 | RelationConfig::FOREIGN_MAPPER => 'content_products',
17 | RelationConfig::AGGREGATES => [
18 | 'products_count' => [
19 | RelationConfig::AGG_FUNCTION => 'count(content_id)',
20 | ]
21 | ]
22 | ],
23 | 'parent' => [
24 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_ONE,
25 | RelationConfig::FOREIGN_MAPPER => 'categories',
26 | ]
27 | ]
28 | ]);
--------------------------------------------------------------------------------
/tests/resources/mappers/content_products.php:
--------------------------------------------------------------------------------
1 | 'content_products',
9 | MapperConfig::PRIMARY_KEY=> 'content_id',
10 | MapperConfig::COLUMNS => ['content_id', 'sku', 'price', 'category_id', 'featured_image_id'],
11 | MapperConfig::CASTS => [
12 | 'content_id' => 'int',
13 | 'category_id' => 'int',
14 | 'featured_image_id' => 'int',
15 | 'price' => 'decimal:2',
16 | ],
17 | MapperConfig::RELATIONS => [
18 | 'category' => [
19 | RelationConfig::FOREIGN_MAPPER => 'categories',
20 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_ONE
21 | ],
22 | 'featured_image' => [
23 | RelationConfig::FOREIGN_MAPPER => 'images',
24 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_ONE
25 | ],
26 | ]
27 | ]);
--------------------------------------------------------------------------------
/tests/resources/mappers/images.php:
--------------------------------------------------------------------------------
1 | 'images',
8 | MapperConfig::COLUMNS => ['id', 'name', 'folder'],
9 | MapperConfig::CASTS => [
10 | 'id' => 'int',
11 | 'content_id' => 'int'
12 | ],
13 | MapperConfig::RELATIONS => [
14 | 'products_where_featured' => [
15 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
16 | RelationConfig::FOREIGN_MAPPER => 'products',
17 | RelationConfig::FOREIGN_KEY => 'featured_image_id'
18 | ]
19 | ]
20 | ]);
--------------------------------------------------------------------------------
/tests/resources/mappers/products.php:
--------------------------------------------------------------------------------
1 | 'content',
9 | MapperConfig::COLUMNS => ['id', 'content_type', 'title', 'description', 'summary'],
10 | MapperConfig::GUARDS => ['content_type' => 'product'],
11 | MapperConfig::RELATIONS => [
12 | 'category' => [
13 | RelationConfig::FOREIGN_MAPPER => 'categories',
14 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_ONE
15 | ],
16 | 'fields' => [
17 | RelationConfig::FOREIGN_MAPPER => 'content_products',
18 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_ONE,
19 | RelationConfig::FOREIGN_KEY => 'content_id',
20 | RelationConfig::LOAD_STRATEGY => RelationConfig::LOAD_EAGER
21 | ],
22 | 'images' => [
23 | RelationConfig::FOREIGN_MAPPER => 'images',
24 | RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
25 | RelationConfig::FOREIGN_GUARDS => ['type' => 'product'],
26 | RelationConfig::AGGREGATES => [
27 | 'images_count' => [
28 | RelationConfig::AGG_FUNCTION => 'count(id)',
29 | ]
30 | ]
31 | ],
32 | 'tags' => [
33 | RelationConfig::FOREIGN_MAPPER => 'tags',
34 | RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_MANY,
35 | RelationConfig::THROUGH_TABLE => 'products_tags',
36 | RelationConfig::THROUGH_NATIVE_COLUMN => 'product_id',
37 | RelationConfig::THROUGH_COLUMNS => ['position'],
38 | RelationConfig::QUERY_CALLBACK => function (Query $query) {
39 | $query->orderBy('position ASC');
40 | return $query;
41 | },
42 | RelationConfig::AGGREGATES => [
43 | 'tags_count' => [
44 | RelationConfig::AGG_FUNCTION => 'count(tags.id)',
45 | ]
46 | ]
47 | ]
48 | ]
49 | ]);
--------------------------------------------------------------------------------
/tests/resources/mappers/tags.php:
--------------------------------------------------------------------------------
1 | 'tags',
8 | MapperConfig::COLUMNS => ['id', 'name'],
9 | // MapperConfig::RELATIONS => [
10 | // 'products' => [
11 | // RelationConfig::FOREIGN_MAPPER => 'products',
12 | // RelationConfig::TYPE => RelationConfig::TYPE_MANY_TO_MANY,
13 | // RelationConfig::THROUGH_COLUMNS => ['position'],
14 | // ]
15 | // ]
16 | ]);
--------------------------------------------------------------------------------
/tests/resources/tables/generic.php:
--------------------------------------------------------------------------------
1 |