Skip to content

Commit

Permalink
Implemented joinWith for joining with related mappers
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrian Miu committed Feb 21, 2020
1 parent a7b476a commit 58257b0
Show file tree
Hide file tree
Showing 42 changed files with 608 additions and 177 deletions.
4 changes: 2 additions & 2 deletions docs/behaviours.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ This would clone the mapper and let you work with it under the new configuration

The Sirius ORM comes with 2 behaviours:

> #### Soft Delete
> ##### Soft Delete
> ```php
> $orm->get('products')
> ->use(new SoftDelete('name of the column with the date of delete'));
> ```
> #### Timestamps
> ##### Timestamps
> ```php
> $orm->get('products')
Expand Down
10 changes: 9 additions & 1 deletion docs/cookbook_custom_entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,23 @@ The hydrator for this entity should be able to transform an array (representing

```php
use Sirius\Orm\Entity\HydratorInterface;
use Sirius\Orm\Entity\EntityInterface;

class CategoryHydrator implements HydratorInterface {

public function newEntity($attributes = []){
public function hydrate($attributes = []){
$category = new Category;
$category->setPk($attributes['id']);
$category->setName($attributes['name']);
$category->setParentId($attributes['parent_id']);
}

public function extract(EntityInterface $entity) {
/**
* Extract entity attributes that are to be persisted to the database
*/
}

}
```

Expand Down
14 changes: 6 additions & 8 deletions docs/cookbook_polymorphic_associations.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,18 @@ use Sirius\Orm\MapperConfig;
use Sirius\Orm\Relation\RelationConfig;

$productsConfig = MapperConfig::fromArray([
MapperConfig::ENTITY_CLASS => Product::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
MapperConfig::TABLE => 'products',
MapperConfig::RELATIONS => [
'comments' => [
RelationConfig::NAME => 'comments',
RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
RelationConfig::FOREIGN_MAPPER => 'comments', // name of the comments mapper as registered in the ORM
RelationConfig::FOREIGN_KEY => 'commentable_id',
RelationConfig::FOREIGN_GUARDS => ['commentable_type' => 'product'], // That's it!
]
]
]);
$this->orm->register('products', $productsConfig);
$orm->register('products', $productsConfig);
```

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'
Expand All @@ -46,25 +45,24 @@ use Sirius\Orm\MapperConfig;
use Sirius\Orm\Relation\RelationConfig;

$productCommentsConfig = MapperConfig::fromArray([
MapperConfig::ENTITY_CLASS => Comment::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\ProductComment',
MapperConfig::TABLE => 'comments',
MapperConfig::GUARDS => ['commentable_type' => 'product']
]);

$productsConfig = MapperConfig::fromArray([
MapperConfig::ENTITY_CLASS => Product::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
MapperConfig::TABLE => 'products',
MapperConfig::RELATIONS => [
'comments' => [
RelationConfig::NAME => 'comments',
RelationConfig::TYPE => RelationConfig::TYPE_ONE_TO_MANY,
RelationConfig::FOREIGN_MAPPER => 'product_comments',
RelationConfig::FOREIGN_KEY => 'commentable_id'
]
]
]);
$this->orm->register('product_comments', $productCommentsConfig);
$this->orm->register('products', $productsConfig);
$orm->register('product_comments', $productCommentsConfig);
$orm->register('products', $productsConfig);
```

This solution relies on the mapper to ensure the proper value is set on the `commentable_type` column at the time it is persisted.
4 changes: 2 additions & 2 deletions docs/cookbook_table_inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ If you don't want the mapper to query multiple entity types you can use [guards]

```php
$pageConfig = MapperConfig::fromArray([
MapperConfig::ENTITY_CLASS => Page::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\Content\Page',
MapperConfig::TABLE => 'content',
MapperConfig::GUARDS => ['content_type' => 'page']
]);
Expand Down Expand Up @@ -40,7 +40,7 @@ class CustomHydrator extends GenericEntityHydrator {
$customEntityHydrator = new CustomHydrator;

$contentConfig = MapperConfig::fromArray([
MapperConfig::ENTITY_CLASS => Content::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\Content',
MapperConfig::TABLE => 'content',
MapperConfig::ENTITY_FACTORY => $customEntityHydrator
]);
Expand Down
7 changes: 7 additions & 0 deletions docs/cookbook_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Cookbook - Testing | Sirius ORM
---

# Cookbook - Testing

TBD
7 changes: 7 additions & 0 deletions docs/cookbook_validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Cookbook - Entity validation | Sirius ORM
---

# Cookbook - Entity validation

TBD
7 changes: 5 additions & 2 deletions docs/couscous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ exclude:

# Base URL of the published website (no "/" at the end!)
# You are advised to set and use this variable to write your links in the HTML layouts
baseUrl: https://www.sirius.ro/php/sirius/sql
baseUrl: https://www.sirius.ro/php/sirius/orm
paypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SGXDKNJCXFPJU
gacode: UA-535999-18

Expand Down Expand Up @@ -75,8 +75,11 @@ menu:
text: Many-to-many
relativeUrl: relation_many_to_many.html
eav:
text: EAV relations
text: EAV
relativeUrl: relation_eav.html
aggregatges:
text: Aggregates
relativeUrl: relation_aggregate.html
cookbook:
name: Cookbook
items:
Expand Down
6 changes: 3 additions & 3 deletions docs/direct_queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Constructing queries work the same as normal queries except the last part where

```php
$orm->get('reviews')
->join('products', 'products.id = reviews.product_id')
->join('INNER', 'products', 'products.id = reviews.product_id')
->where('products.name', 'Gold', 'contains')
->groupBy('product_id')
->select('product_id', 'AVERAGE(rating) as rating')
Expand All @@ -33,7 +33,7 @@ Here's another example for counting some matching rows:

```php
$orm->get('reviews')
->join('products', 'products.id = reviews.product_id')
->join('INNER', 'products', 'products.id = reviews.product_id')
->where('products.name', 'Gold', 'contains')
->select('COUNT(reviews.id) as total_gold_reviews')
->fetchValue();
Expand All @@ -45,7 +45,7 @@ You can reuse queries to minimise the potential for errors:

```php
$query = $orm->get('reviews')
->join('products', 'products.id = reviews.product_id')
->join('INNER', 'products', 'products.id = reviews.product_id')
->where('products.name', 'Gold', 'contains')
->groupBy('product_id');

Expand Down
12 changes: 12 additions & 0 deletions docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ $product->category->name = 'New category name'; // this works with lazy loading
$product->images->get(0)->path = 'new_image.jpg'; // this too
```

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

```php
if (!$product->images->isEmpty()) {
$product->images->first();
}

$paths = $product->images->map(function($image) {
return $image->path;
});
```

## Persisting the entities

```php
Expand Down
4 changes: 2 additions & 2 deletions docs/mappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ use Sirius\Orm\MapperConfig;
use Sirius\Orm\Relation\RelationConfig;

$orm->register('products', MapperConfig::fromArray(
MapperConfig::ENTITY_CLASS => Product::class,
MapperConfig::ENTITY_CLASS => 'App\Entity\Product',
MapperConfig::TABLE => 'tbl_products',
MapperConfig::TABLE_ALIAS => 'products', // if you have tables with prefixes
MapperConfig::PRIMARY_KEY => 'product_id', // defaults to 'id'
MapperConfig::COLUMNS => ['id', 'name', 'price', 'sku'],
MapperConfig::CASTS => ['id' => 'integer', 'price' => 'decimal:2'],
MapperConfig::COLUMN_ATTRIBUTE_MAP => ['sku' => 'code'], // the entity works with the 'code' attribute
MapperConfig::GUARDS => ['published' => 1], // see "The guards" page
MapperConfig::SCOPES => ['sortRandom' => $callback], // see "The query scopes" page
Expand All @@ -41,7 +42,6 @@ $orm->register('products', MapperConfig::fromArray(
],
MapperConfig::RELATIONS => [
'images' => [
RelationConfig::NAME => 'images',
RelationConfig::FOREIGN_MAPPER => 'images'
// see the Relation section for the rest
]
Expand Down
76 changes: 64 additions & 12 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ title: Searching mappers | Sirius ORM

Searching for entities is pretty simple. If you have experience with other query builders you should be comfortable writting queries right away

Under the hood Sirius ORM uses [Sirius\Sql](https://www.sirius.ro/php/sirius/sql/) and all querying capabilities there are also available in the ORM.
Under the hood Sirius ORM uses [Sirius\Sql](https://www.sirius.ro/php/sirius/sql/) and all its querying are also available in the ORM.

**Important!** If you decide to use the `fetch` and `yield` methods keep in mind they won't return entities but table rows.
Only the `find`, `first`, `get` and `paginate` will process the rows through the `EntityHydrator` object.
See [direct queries](direct_queries.md) for further details.

## Finding entities by primary key
##### Finding entities by primary key

```php
$orm->find('products', 1);
Expand All @@ -22,7 +22,7 @@ $orm->get('products')->find(1);

This returns a single entity or NULL.

## Searching entities
##### Searching entities

```php
$orm->get('products')
Expand All @@ -43,7 +43,7 @@ $orm->get('products')
```


## Paginating entities
##### Paginating entities

Pagination is similar to searching except it returns a `PaginatedCollection` that also contains pagination information (total entities matching the query, current page, total pages, items per page)

Expand All @@ -69,7 +69,6 @@ $orm->get('products')
->load('category.parent') // go as deep as you want
->load([
// use a callback to alter the relation query
// in this case we only want the first 2 images
'images' => function($query) {
$query->orderBy('priority')->limit(2);
return $query;
Expand All @@ -87,15 +86,37 @@ $orm->get('products')
'category.parent',
[
// use a callback to alter the relation query
// in this case we only want the first 2 images
'images' => function($query) {
$query->orderBy('priority')->limit(2);
$query->orderBy('priority');
return $query;
}
]
);
```


**Warning!** Using `limit` and `offset` in the query callback does not work as you might expect when you want more than 1 entity back (ie: when you are not using `find` or `first`).

```php
$orm->get('products')
->where('category_id', 10)
->load(
'category',
'category.parent',
[
// use a callback to alter the relation query
'images' => function($query) {
$query->orderBy('priority')
->limit(2); // <---- NOT RIGHT
return $query;
}
]
);
```

The above code will not return 2 images per product but 2 images for the entire query. If the query returns 10 products only 2 images will be found by the subsequent query.


If you want to use eager-loading with `find` you need to specify it as the second argument

```php
Expand All @@ -104,20 +125,51 @@ $orm->get('products')
'category',
'category.parent',
'images' => function($query) {
$query->orderBy('priority')->limit(2);
$query->orderBy('priority');
return $query;
}
]);
```

**Note!** Using `limit` and `offset` on queries of type `find` or `first` will work as expected.

## JOINing mappers

If you want to join related mappers you have to use `joinWith`

```php
$orm->select('products')
->joinWith('images')
->where('images.approved', 1)
->get();
```

Things to consider:

1. The ORM will create a subselect that is JOINed with the "main" table and it is referenced AS the name of the relation, NOT the underlying table. Keep this in mind when you do `where`, `orderBy` etc.
2. The ORM will not make any `groupBy` calls so you are in charge of this. If you join with a one-to-many or many-to-many relations you need to issue a `groupBy` so you don't get duplicates. The ORM can't make this decision for you since it doesn
't know the purpose of the join

## JOINing tables

If you want to query entities and use JOINs you have to specify the table names as they are. At the moment, Sirius ORM convert joined mappers to tables in the queries. If your category table is called `tbl_categories` you have to specify it like
it is
Although not recommended you can still issue joins with other tables.

```php
$products = $orm->select('products')
->join('tbl_categories as categories', 'categories.id = products.id')
->join('INNER', 'tbl_categories as categories', 'categories.id = products.id')
->where('categories.id', 10)
->get();
```
```

## Aggregates and aditional columns

If you do any joins you may want to include additional columns in the results using the `columns()` method. Those columns will be attaches "as is" to the entity, if the mapper's `EntityHydrator` allows it

```php
$products = $orm->select('products')
->joinWith('images')
->where('images.approved', 1)
->groupBy('products.id')
->columns('COUNT(images.id) AS images_count')
->get();
```
11 changes: 11 additions & 0 deletions docs/relation_aggregate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Relation aggregates | Sirius ORM
---

# Relation aggregates

This feature is WIP.

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
stats table or in special columns) but sometimes your app doesn't need this type of optimizations.

8 changes: 6 additions & 2 deletions docs/relation_eav.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
---
title: Entity-Attribute-Value | Sirius ORM
---

# Entity-Attribute-Value relations

This feature is WIP.

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
. The EAV table has one column for the name of the attribute and another with the value.

Expand All @@ -15,5 +21,3 @@ $product->meta_title = "SEO title";
```

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.

This feature is WIP.

0 comments on commit 58257b0

Please sign in to comment.