-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
[RFC] Converting Active Record to the Data Mapper pattern #12317
Conversation
Introduce please repository class imho as well, because using modelsManager is dirty. So we can still jus t do abstract class ModelRepository
{
// all find methods, as well findFirstById, and findFirstBy*, etc, just all finds which are currently in model, as well sum, count etc things
abstract public funciton setSource(); // which will return Person::class for example in PersonRepository, this will be used for all selections
} Or eventually this should be used in model like And then totally remove this Also we should aim for totally removing Also repository classes will be good one place for adding all custom related model find methods, so somewhere in code instead of executing some extensive find or query we can just call proper method - this is pretty main reason im asking for it, just built-in repository class with some generic methods in it + option to add our own. Don't know which way is better, i guess second. Also this change is of course for 4.0.0. I thought @andresgutierrez was planning this for 4.0.0 I would need to rewrite whole app then because im using everywhere |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me
phalcon/mvc/model/manager.zep
Outdated
|
||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too much spacing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going for speed. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for speed, it will be at least few months until we create 4.0.x repo i guess, and few years to release :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 77acbfe.
phalcon/mvc/model/manager.zep
Outdated
|
||
|
||
/** | ||
* Allows to query the first record that match the specified conditions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allows to query for the first record that matches the specified conditions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 77acbfe.
@Jurigag, it'd be tedious to have to create repository classes for each model. We could set the model class name in the constructor though: namespace Phalcon\Mvc\Model;
class Repository
{
public function __construct(string! modelClass);
public function find(array params = []) -> <ResultsetInterface>;
public function findFirst(array params = []) -> <ModelInterface>;
public function count(array parameters = []) -> int;
public function sum(array parameters = []);
public function maximum(array parameters = []);
public function minimum(array parameters = []);
public function average(array parameters = []);
} It would reduce the complexity of the Models Manager a bit but would repositories add complexity for users/developers?: $robots = $modelsManager->findFirst(
Robots::class,
$params
);
// OR ...
$robotsRepository = $modelsManager->getRepository(
Robots::class
);
$robots = $robotsRepository->findFirst($params); |
I mean if you don't want - you don't need to create repository. You can use as well just By using constructor it's fine. But what if we want to extend phalcon repository and use our own ? Then we need to point it somewhere, in model or in modelsmanager. SO: We don't need to create repositories for each model and generic internal Repository will be used for each model. But still we will have in model something like Or eventually something like I think second will be better so we can like remove totally need for people to extend So in total - i think there should be Repository class which will be used always. No matter if we create it or not. Just phalcon will create proper repository object if none provided by us. I know that this can mean that there could be possible Also models manager is already really complex enough. So introducing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Phalcon/Mvc/Model/Repository
class, add option to use custom Repository class for models.
phalcon/mvc/model/manager.zep
Outdated
*/ | ||
let related = model->_related; | ||
if typeof related == "array" { | ||
if model->_preSaveRelatedRecords(writeConnection, related) === false { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
/** | ||
* We need to check if the record exists | ||
*/ | ||
let exists = model->_exists(metaData, readConnection, table); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
* Depending if the record exists we do an update or an insert operation | ||
*/ | ||
if exists { | ||
let success = model->_doLowUpdate(metaData, writeConnection, table); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
/** | ||
* Save the post-related records | ||
*/ | ||
let success = model->_postSaveRelatedRecords(writeConnection, related); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
* _postSave() invokes after* events if the operation was successful | ||
*/ | ||
if globals_get("orm.events") { | ||
let success = model->_postSave(success, exists); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
if success === false { | ||
model->_cancelOperation(); | ||
} else { | ||
model->fireEvent("afterSave"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
phalcon/mvc/model/manager.zep
Outdated
} | ||
|
||
if success === false { | ||
model->_cancelOperation(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be something like Phalcon\Mvc\Model\Worker
or Phalcon\Mvc\Model\UnitOfWork
which will do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worker class for model methods:
- _preSaveRelatedRecords
- _exists
- _preSave
- _doLowUpdate
- _doLowInsert
- _postSaveRelatedRecords
- _postSave
- _cancelOperation
- fireEvent
- _checkForeignKeysReverseRestrict
- _checkForeignKeysReverseCascade
So we can remove all this not needed logic in model. It should impelemnt EvensAwareInterface
so we can attach some listener to it. Not sure about which events exactly - i guess preSave, postSave, onCancelOperation, onUpdate, onInsert ? Also as well existing model events so we can watch all model events fired for all models in one listener.
phalcon/mvc/model/manager.zep
Outdated
{ | ||
var repository; | ||
|
||
let repository = new Repository(modelClass, this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be property _repositories im Manager which will be array of repostiories where all repositories should be added and their should be checked before creating new one. Also add method registerRepository(<RepositoryInterface> repository)
which will add option to use custom repository for models. This will need to add modelClass property to Repository class so we can retrieve it from repository when it's constructed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test cases for class implementing only ModelInterface not extending Model, in future for any class without any interface or extending class.
What uses could a custom repository have? In my head, I imagine Repository only has the methods listed above (as well as support for findFirstBy..., findBy..., countBy..., etc;). It might be a good idea to allow the developer to overwrite the repository class via the DI: $di->set("modelRepository", "Phalcon\\Mvc\\Model\\Repository");
// OR...
$di->set("Phalcon\\Mvc\\Model\\Repository", "MyCustomRepositoryClass"); let repository = <RepositoryInterface> dependencyInjector->get(
"modelRepository",
[modelClass, this]
);
// OR...
let repository = <RepositoryInterface> dependencyInjector->get(
"Phalcon\\Mvc\\Model\\Repository",
[modelClass, this]
); |
Repository also should provide query builder(like class PersonRepository extends Repository
{
public function findWithProfiles()
{
return $this->createBuilder()->from('Person')
->columns('*')
->leftJoin('Profile')
->getQuery()
->execute();
}
// any other methods we need for finding persons and where generic ones are not enough
} Just add Your solution is too generic. There should be for 100% option to have each Model his own Repository. Otherwise it's useless Repository pattern. So in total Repository class:
In Manager:
$manager->registerRepository(new PersonRepository(Person::class)); which will add this repository to repositories, also it could be good idea to maybe lazy load it like
Also maybe registerRepository is bad idea and better will be something like: class Person
{
public static function getRepository()
{
return new PersonRepository(self::class);
}
} |
Also this is really great idea. But this definitely need a hard work and implement new classes. It's not only change a few we have right now = and we have data mapper. I just add my ideas what i think should be here and how it should look. How it will be implemented and what's your idea it's up to you and someone who will merge this. So just don't lose your enthusiasm even i maybe write demotivating comments or something :D |
It's been a long day and after spending so much time looking at code, I can't see straight anymore. I need a good night's sleep before I can do any more serious coding. 😝 I agree with your ideas about custom repositories though. How about copying how the Models Manager deals with custom model sources and schemas ( class Robots extends \Phalcon\Mvc\Model
{
public function initialize()
{
$this->setRepository(
RobotsRepository::class
);
}
} I think I'll use this PR as a playground for ideas and redo the entire thing when the 4.0.x branch is created and everyone's opinions are taken into account. |
Yea This is good way BUT i'm not sure about $this->setRepository. DataMapper is all about NOT EXTENDING any class i think. I think in result and in final form the only needed thing for our models in phalcon 4.0.0 should be a) implement interface b) use trait c) don't use anything. Im just not sure about solution. For NOW your solution is fine, but i think we should avoid |
I'm glad you're here - you're right and I'm thinking of solutions that require the least amount of effort to implement. 😉 I've never used traits but I seem to remember that it's akin to copying and pasting chunks of code, as far as the compiler is concerned. We'll figure it out tomorrow. |
But im not sure, maybe there should be like totally nothing, this is up to people with more knowledge, don't know if DataMapper pattern allows anything like trait or something like this. Well in doctrine there are annotations, and something is parsing them, so well - this is just some kind of "workaround" in doctrine to not having trait or something like this. |
That's kind of the problem. Doctrine stores a lot of metadata in annotations and I don't think annotations will be considered fast enough. We could have metadata stored somewhere else (YAML, XML, ...) but that'd be messy. Or we could just have a few getters in Model. |
Or just trait and still use $this->xyz but without extends. |
Well it's not so easy, but maybe this is a FINALLY time to introduce traits to zephir ? @andresgutierrez @sergeyklay we are waiting already 2 years for it zephir-lang/zephir#504 |
Well this is why we have zephir and we can create something which will be still fast enough and will get all advantages of Datamapper - but it should be a real Datamapper or be really similar to it as close as it can be. I already like changes which are done here - but there are still some job to do. |
abandoned project https://github.com/lynx/lynx-legacy-zephir |
@SidRoberts also if you will return to this it could be nice to add some event like notFound or something if result of |
How about introducing this slowly over the course of version 3, keeping BC along the way and then drop BC or deprecate in version 4? Or would that be too complicated? |
Too complicated in my opinion. Better just create 4.0.x branch and merge it while it's be done and that's it. Also as i already wrote we need class like Worker or something like this for all those |
I've been doing a lot of reading lately about data mapper vs active record and the more I read the more I like the data mapper pattern. Though I do not like doctrine due to all the dang @annotations. Is it your goal to replace active record with data mapper pattern or will both be available ? Will the domain models be required to extend a base class? |
Just a note theres no way the active record can be replaced in Phalcon as this is one of the core features of the framework. Any new ORM will need to be an additional option that doesn't impact existing active record. The debate between active record and data mapper is highly contentious and not something that will just change regardless of major version bump. |
I completely messed up the history and there were too many merge conflicts to deal with so I'm recreating these commits manually. The original branch is available at https://github.com/SidRoberts/cphalcon/commits/v4-model-data-mapper-original |
@SidRoberts Could you please check tests |
Thank you |
I think the better option is move pagination method to this repository / modelsManager or make new class (PagingRepository) |
So I thought I'd have a go at converting Phalcon's Active Record models to the Data Mapper pattern (see Doctrine) and I want to hear people's thoughts and opinions before I take it any further.
I've avoided doing to much work just in case it ends up being a dead-end so no code has been refactored and methods in the models manager now call protected methods in Phalcon\Mvc\Model, etc; - it's a bit of a mess. Obviously this will/should all change.
Put succinctly, methods have been moved to the models manager and (currently) operate exactly as they did before:
Phalcon\Mvc\Model is still far too complicated, in my opinion, so there's still a lot of work to do. 😛
A few things that I'd like to do is remove the ability to assign properties during create/update/save:
I know people like it because it acts a little shortcut but it doesn't make sense in the data mapper context and looks a little clumsy. A method called
save()
should save and do nothing else.I'd also like to decide between only supporting public properties or getters/setters but have no personal preference of either. Alternatively (or additionally), make an effort to vastly simplify how properties are get/set internally.
(It goes without saying but is obviously aimed at version 4 - not version 3)
Thoughts?