Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model has-one-through relations #14511

Merged
merged 9 commits into from
Nov 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
- Added `Phalcon\Html\Link\Serializer\Header`
- Added `Phalcon\Html\Link\Serializer\SerializerInterface`
- Added `Phalcon\Collection:getKeys` and `Phalcon\Collection\getValues` for getting data from the collection [#14507](https://github.com/phalcon/cphalcon/issues/14507)
- Added has-one-through relations to `Phalcon\Mvc\Model` and `Phalcon\Mvc\Model\Manager` [#14511](https://github.com/phalcon/cphalcon/pull/14511)
- Added `Phalcon\Mvc\Model::hasOneThrough()`
- Added `Phalcon\Mvc\Model\Manager::addHasOneThrough()`
- Added `Phalcon\Mvc\Model\Manager::existsHasOneThrough()`
- Added `Phalcon\Mvc\Model\Manager::getHasOneThrough()`
- Added `Phalcon\Mvc\Model\ManagerInterface::addHasOneThrough()`
- Added `Phalcon\Mvc\Model\ManagerInterface::existsHasOneThrough()`
- Added `Phalcon\Mvc\Model\ManagerInterface::getHasOneThrough()`

## Changed
- Changed `Phalcon\Paginator\Adapter\Model`
Expand Down
64 changes: 62 additions & 2 deletions phalcon/Mvc/Model.zep
Original file line number Diff line number Diff line change
Expand Up @@ -4725,7 +4725,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
columns, referencedModel, referencedFields, relatedRecords, value,
recordAfter, intermediateModel, intermediateFields,
intermediateValue, intermediateModelName,
intermediateReferencedFields;
intermediateReferencedFields, existingIntermediateModel;
bool isThrough;

let nesting = false,
Expand Down Expand Up @@ -4754,7 +4754,7 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
connection->rollback(nesting);

throw new Exception(
"Only objects/arrays can be stored as part of has-many/has-one/has-many-to-many relations"
"Only objects/arrays can be stored as part of has-many/has-one/has-one-through/has-many-to-many relations"
);
}

Expand Down Expand Up @@ -4854,6 +4854,23 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
intermediateModelName
);

/**
* Has-one-through relations can only use one intermediate model.
* If it already exist, it can be updated with the new referenced key.
*/
if relation->getType() == Relation::HAS_ONE_THROUGH {
let existingIntermediateModel = intermediateModel->findFirst(
[
"[" . intermediateFields . "] = ?0",
"bind": [value]
]
);

if existingIntermediateModel {
let intermediateModel = existingIntermediateModel;
}
}

/**
* Write value in the intermediate model
*/
Expand Down Expand Up @@ -5160,6 +5177,49 @@ abstract class Model extends AbstractInjectionAware implements EntityInterface,
);
}

/**
* Setup a 1-1 relation between two models, through an intermediate
* relation
*
*```php
* class Robots extends \Phalcon\Mvc\Model
* {
* public function initialize()
* {
* // Setup a 1-1 relation to one item from Parts through RobotsParts
* $this->hasOneThrough(
* "id",
* RobotsParts::class,
* "robots_id",
* "parts_id",
* Parts::class,
* "id",
* );
* }
* }
*```
*
* @param string|array fields
* @param string|array intermediateFields
* @param string|array intermediateReferencedFields
* @param string|array referencedFields
* @param array options
*/
protected function hasOneThrough(var fields, string! intermediateModel, var intermediateFields, var intermediateReferencedFields,
string! referenceModel, var referencedFields, options = null) -> <Relation>
{
return (<ManagerInterface> this->modelsManager)->addHasOneThrough(
this,
fields,
intermediateModel,
intermediateFields,
intermediateReferencedFields,
referenceModel,
referencedFields,
options
);
}

/**
* Sets if the model must keep the original record snapshot in memory
*
Expand Down
201 changes: 198 additions & 3 deletions phalcon/Mvc/Model/Manager.zep
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use Phalcon\Mvc\Model\ResultsetInterface;
use Phalcon\Mvc\Model\ManagerInterface;
use Phalcon\Di\InjectionAwareInterface;
use Phalcon\Events\EventsAwareInterface;
use Phalcon\Mvc\Model\Query;
use Phalcon\Mvc\Model\QueryInterface;
use Phalcon\Mvc\Model\Query\Builder;
use Phalcon\Mvc\Model\Query\BuilderInterface;
Expand Down Expand Up @@ -108,6 +109,16 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
*/
protected hasOneSingle = [];

/**
* Has one through relations
*/
protected hasOneThrough = [];

/**
* Has one through relations by model
*/
protected hasOneThroughSingle = [];

/**
* Mark initialized models
*/
Expand Down Expand Up @@ -764,6 +775,125 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
return relation;
}

/**
* Setups a relation 1-1 between two models using an intermediate model
*
* @param string fields
* @param string intermediateFields
* @param string intermediateReferencedFields
* @param string referencedFields
* @param array options
*/
public function addHasOneThrough(<ModelInterface> model, var fields, string! intermediateModel,
var intermediateFields, var intermediateReferencedFields, string! referencedModel, var referencedFields, var options = null) -> <RelationInterface>
{
var entityName, referencedEntity, hasOneThrough, relation, relations,
alias, lowerAlias, singleRelations, intermediateEntity;
string keyRelation;

let entityName = get_class_lower(model),
intermediateEntity = strtolower(intermediateModel),
referencedEntity = strtolower(referencedModel),
keyRelation = entityName . "$" . referencedEntity;

let hasOneThrough = this->hasOneThrough;

if !fetch relations, hasOneThrough[keyRelation] {
let relations = [];
}

/**
* Check if the number of fields are the same from the model to the
* intermediate model
*/
if typeof intermediateFields == "array" {
if unlikely count(fields) != count(intermediateFields) {
throw new Exception(
"Number of referenced fields are not the same"
);
}
}

/**
* Check if the number of fields are the same from the intermediate
* model to the referenced model
*/
if typeof intermediateReferencedFields == "array" {
if unlikely count(fields) != count(intermediateFields) {
throw new Exception(
"Number of referenced fields are not the same"
);
}
}

/**
* Create a relationship instance
*/
let relation = new Relation(
Relation::HAS_ONE_THROUGH,
referencedModel,
fields,
referencedFields,
options
);

/**
* Set extended intermediate relation data
*/
relation->setIntermediateRelation(
intermediateFields,
intermediateModel,
intermediateReferencedFields
);

/**
* Check an alias for the relation
*/
if fetch alias, options["alias"] {
if typeof alias != "string" {
throw new Exception("Relation alias must be a string");
}

let lowerAlias = strtolower(alias);
} else {
let lowerAlias = referencedEntity;
}

/**
* Append a new relationship
*/
let relations[] = relation;

/**
* Update the global alias
*/
let this->aliases[entityName . "$" . lowerAlias] = relation;

/**
* Update the relations
*/
let this->hasOneThrough[keyRelation] = relations;

/**
* Get existing relations by model
*/
if !fetch singleRelations, this->hasOneThroughSingle[entityName] {
let singleRelations = [];
}

/**
* Append a new relationship
*/
let singleRelations[] = relation;

/**
* Update relations by model
*/
let this->hasOneThroughSingle[entityName] = singleRelations;

return relation;
}

/**
* Setup a relation reverse many to one between two models
*
Expand Down Expand Up @@ -1130,6 +1260,31 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
return isset this->hasOne[keyRelation];
}

/**
* Checks whether a model has a hasOneThrough relation with another model
*/
public function existsHasOneThrough(string! modelName, string! modelRelation) -> bool
{
var entityName;
string keyRelation;

let entityName = strtolower(modelName);

/**
* Relationship unique key
*/
let keyRelation = entityName . "$" . strtolower(modelRelation);

/**
* Initialize the model first
*/
if !isset this->initialized[entityName] {
this->load(modelName);
}

return isset this->hasOneThrough[keyRelation];
}

/**
* Checks whether a model has a hasManyToMany relation with another model
*/
Expand Down Expand Up @@ -1243,7 +1398,7 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
var referencedModel, intermediateModel, intermediateFields, fields,
builder, extraParameters, refPosition, field, referencedFields,
findParams, findArguments, uniqueKey, records, arguments, rows,
firstRow;
firstRow, query;
array placeholders, conditions, joinConditions;
bool reusable;
string retrieveMethod;
Expand Down Expand Up @@ -1331,9 +1486,19 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI

/**
* Get the query
* Execute the query
*/
return builder->getQuery()->execute();
let query = <QueryInterface> builder->getQuery();

switch relation->getType() {
case Relation::HAS_MANY_THROUGH:
return query->execute();

case Relation::HAS_ONE_THROUGH:
return query->setUniqueRow(true)->execute();

default:
throw new Exception("Unknown relation type");
}
}

let conditions = [];
Expand Down Expand Up @@ -1606,6 +1771,20 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
return relations;
}

/**
* Gets hasOneThrough relations defined on a model
*/
public function getHasOneThrough(<ModelInterface> model) -> <RelationInterface[]> | array
{
var relations;

if !fetch relations, this->hasOneThroughSingle[get_class_lower(model)] {
return [];
}

return relations;
}

/**
* Gets hasManyToMany relations defined on a model
*/
Expand Down Expand Up @@ -1669,6 +1848,15 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
}
}

/**
* Get has-one-through relations
*/
if fetch relations, this->hasOneThroughSingle[entityName] {
for relation in relations {
let allRelations[] = relation;
}
}

/**
* Get many-to-many relations
*/
Expand Down Expand Up @@ -1712,6 +1900,13 @@ class Manager implements ManagerInterface, InjectionAwareInterface, EventsAwareI
return relations;
}

/**
* Check whether it's a has-one-through relationship
*/
if fetch relations, this->hasOneThrough[keyRelation] {
return relations;
}

return false;
}

Expand Down
Loading