What is the best practice to add custom methods to the querybuilder based upon existing querybuilder methods? - doctrine

in the following code example i use a custom repository method where i have to call it like this $queryBuilder = $this->addFilterByVisible($queryBuilder); but i would like to call it like that $queryBuilder->andWhere(...)->addFilterByVisible()
is this possible? is this possible without dealing with AST and adding custom dql functions like descibed here https://www.doctrine-project.org/2010/03/29/doctrine2-custom-dql-udfs.html
is the solution to simply extend the querybuilder and overriding the createQueryBuilder method?
this is the current "full" example.
class PersonRepository extends ServiceEntityRepository
{
protected function addFilterByVisible(QueryBuilder $queryBuilder): QueryBuilder
{
$queryBuilder
->andWhere('person_group.visible = :group_is_visible')
->andWhere('person.visible = :person_is_visible')
->setParameter('group_is_visible', true)
->setParameter('person_is_visible', true)
;
return $queryBuilder;
}
protected function queryAllSorted()
{
$queryBuilder = $this->createQueryBuilder('person');
$queryBuilder
->innerjoin('person.group', 'person_group')
->addSelect('person_group')
;
$queryBuilder = $this->addFilterByVisible($queryBuilder);
return $queryBuilder;
}
}
for reference: how to create a custom querybuilder
Custom QueryBuilder in Doctrine

Related

Yii2: How to set default attribute values in ActiveRecord?

This may seem like a trivial question, however all of the obvious solutions that I can think of have their own flaws.
What we want is to be able to set any default ActiveRecord attribute value for new records only, in a way that makes it readable before and during validation and does not interfere with derived classes used for search.
The default values need to be set and ready as soon as we instantiate the class, so that (new MyModel)->attr returns the default attr value.
Here are some of the possibilities and the problems they have:
A) In MyModel override the init() method and assign default value when isNewRecord is true like so:
public function init() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
parent::init();
}
Problem: Search. Unless we explicitly unset our default attribute in MySearchModel (very error-prone because it is too easy to forget), this will also set the value before calling search() in the derived MySearchModel class and interfere with searching (the attr attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by calling unsetAttributes() before calling search(), however no such method exists in Yii2.
B) In MyModel override the beforeSave() method like so:
public function beforeSave($insert) {
if ($insert) {
$this->attr = 'defaultValue';
}
return parent::beforeSave();
}
Problem: Attribute is not set in unsaved records. (new MyModel)->attr is null. Worse yet, even other validation rules that rely on this value will not be able to access it, because beforeSave() is called after validation.
C) To ensure the value is available during validation we can instead override the beforeValidate() method and set the default values there like so:
public function beforeValidate() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
return parent::beforeValidate();
}
Problem: Attribute is still not set in unsaved (unvalidated) records. We need to at least call $model->validate() if we want to get the default value.
D) Use DefaultValidator in rules() to set a default attribute value during validation like so:
public function rules() {
return [
[
'attr', 'default',
'value' => 'defaultValue',
'on' => 'insert', // instantiate model with this scenario
],
// ...
];
}
Problem: Same as B) and C). Value is not set until we actually save or validate the record.
So what is the right way to set default attribute values? Is there any other way without the outlined problems?
There's two ways to do this.
$model => new Model();
Now $model has all the default attributes from the database table.
Or in your rules you can use:
[['field_name'], 'default', 'value'=> $defaultValue],
Now $model will always be created with the default values you specified.
You can see a full list of core validators here http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html
This is a hangup with Yii's bloated multi-purpose ActiveRecords
In my humble opinion the form models, active records, and search models would be better off split into separate classes/subclasses
Why not split your search models and form models?
abstract class Creature extends ActiveRecord {
...
}
class CreatureForm extends Creature {
public function init() {
parent::init();
if ($this->isNewRecord) {
$this->number_of_legs = 4;
}
}
}
class CreatureSearch extends Creature {
public function search() {
...
}
}
The benefits of this approach are
You can easily cater for different validation, set up and display cases without resorting to a bunch of ifs and switches
You can still keep common code in the parent class to avoid repetition
You can make changes to each subclass without worrying about how it will affect the other
The individual classes don't need to know about the existence of any of their siblings/children to function correctly
In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all
I know it is answered but I will add my approach.
I have Application and ApplicationSearch models. In Application model I add init with a check of the current instance. If its ApplicationSearch I skip initializations.
public function init()
{
if(!$this instanceof ApplicationSearch)
{
$this->id = hash('sha256', 123);
}
parent::init();
}
also as #mae commented below you can check for existence of search method in current instance, assuming you didn't add any method with name search to the non-search base model so the code becomes:
public function init()
{
// no search method is available in Gii generated Non search class
if(!method_exists($this,'search'))
{
$this->id = hash('sha256', 123);
}
parent::init();
}
I've read your question several times and I think there are some contradictions.
You want the defaults to be readable before and during validation and then you try init() or beforeSave(). So, assuming you just want to set the default values in the model so they can be present during the part of the life cycle as long as possible and not interfere with the derived classes, simply set them after initialising the object.
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
$model->setDefaultValues();
Or you can create static method to create model with all default values set and return the instance of it.
$model = Model::createNew();
Or you can pass default values to constructor.
$model = new Model([
'attribute1' => 'value1',
'attribute2' => 'value2',
]);
This is not much different from setting the attributes directly.
$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';
Everything depends on how much transparent would you like your model be to your controller.
This way attributes are set for the whole life cycle except the direct initialisation and it's not interfering with derived search model.
Just override __construct() method in your model like this:
class MyModel extends \yii\db\ActiveRecord {
function __construct(array $config = [])
{
parent::__construct($config);
$this->attr = 'defaultValue';
}
...
}
If you want to load default value from database you can put this code in your model
public function init()
{
parent::init();
if(!method_exists($this,'search')) //for checking this code is on model search or not
{
$this->loadDefaultValues();
}
}
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
if($model->isNewRecord())
$model->setDefaultValues();

Laravel Query builder and table names

I noticed that I write the database table names quite a lot, and in different files, when I use the Query Builder. If I were to change the database table names, I would have to search and change quite many rows in my project.
Is this an issue your Laravel guys noticed and come up with an solution to?
I like the Eloquent approach which uses class models, instead of database names; but for some queries I think the Query Builder is a better solution (though I am no expert in this matter).
If you already have a queryBuilder object you can obtain the table name like
$tableName = $query->getModel()->getTable();
Use this in your query :
(new YourModel())->getTable()
Example :
DB:raw('SELECT * FROM '.(new User())->getTable().' WHERE id=3');
How about using OOP concept. Laravel is a framework, so no one stops you from using basic PHP OOP concept. This is what I do:
Consider my query is like :
$result=DB::table('myTable')->select()->get();
What I do is make a class that holds all the tablenames :
class TableName
{
private $tableName= "myTable";
public function getTableName()
{
return $this->tableName;
}
public function setTableName($table_name)
{
$this->tableName = $table_name;
}
}
Now all i have to do is call a method using an object in the file I want to use the table like :
$name = new TableName() ;
$result=DB::table($name->getTableName())->select()->get();
Use wherever you want. I don't think its the best solution however it works for me. Hope it helps
Maybe you can extend the model class.
CModel extend Model {
protected static $tableName;
public static getTableName(){
if(static::$tableName)
return static::$tableName;
/* if you create a "reference break" you don't have to *
/* create "protected static $tableName" row in your all model */
$table = (new static())->getTable();
return static::$tableName = &$table;
}
}
YourModel extends CModel {...}
than you can use
YourModel::getTableName()
I'm not have better idea.

Ordering table data

I already read this piece from the laravel 5.1 documentation:
$users = DB::table('users')
->orderBy('name', 'desc')
->get();
I have no ideia where to write that.
And this is what I tried to write inside my Model:
class Professor extends Model
{
$professor = DB:table('professor')->orderBy('name','asc')->get();
}
Also tried:
class Professor extends Model
{
Professor::orderBy('name')->get();
//$professor = Professor::orderBy('name')->get();
}
Nothing works e.e
All of them give me erros like:
syntax error, unexpected '$professor' (T_VARIABLE), expecting function (T_FUNCTION)
The piece of code your trying to write should not be placed inside a Model. It should be in a controller or a repository if your using a Repository Pattern.
Assuming you got the following in your code.
A table called professors. A model Professor . A Controller called ProfessorsController.
And a route file with the following code get('professors','ProfessorsController#index');
Then on the controller you should have the following code.
class ProfessorsController extends Controller {
public function index()
{
$professors = \DB:table('professors')->orderBy('name','asc')->get();
return view('proffesors')->with('proffessors',$professors);
}
}
This will return an order list of professors to the view. That is if you are using a view to represent the data.
It does not need to be in model. Most of time something like that goes in controller.
In model you need to define relations or functions that would be used application wide on a object.
If you want to do something similar in model you won't use DB::table you need something like:
class Professor extends \Eloquent
{
public function professorsByName(){
$professionCollection = Professor::all()->orderBy('name','asc')->get();
return $professionCollection;
}
}
Please take it as example it's not something that should go in model at least this simple not.
Mental Note
Never use DB::table reason your observer if any won't execute.
I think you all are missing the primary issue with
class Professor extends Model
{
$professor = DB:table('professor')->orderBy('name','asc')->get();
}
This is not how PHP classes work.
OP, you need to brush up on the concepts of OOP.
What you need is something like this:
your controller:
public function myRoutedMethod()
{
$professors = Professor::getModel()->orderBy('name','asc')->get();
foreach($professors as $professor)
{
var_dump($professor->toArray();
}
}
Or probably even better, create a repository to interface with your model and just call $repository->professors()->toArray();
Google search: Laravel Repository

How to decouple eloquent from the service layer?

I am attempting to create a clean cut service layer, whereby the service layer acts upon one or more repositories, and each repositories acts on its own eloquent model.
For example, I may have:
ForumService
|
+-- PostRepo extends PostInterface
| |
| +-- Post (Eloquent)
|
+-- UserRepo extends UserInterface
|
+-- User (Eloquent)
Each service defines it's required dependencies via ioc. So, something like:
// MessageService
// ..
public function __construct(UserInterface $userRepository,
MessageInterface $messageRepository) {
// ..
}
My repositories are resolved via their bindings in their respective service providers, such as:
class UserRepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app>bind(
'App\Models\Repositories\User\UserInterface',
'App\Models\Repositories\User\UserRepository');
}
}
This all works just fine. Each service gets the repositories it requires.
To keep the service layer clear of any specific dependency on eloquent, anything that leaves a repo is a simple, immutable, data object.
Key points in everyday language:
Only the repo's talk to their own models directly
Repo's return simple, immutable, data objects
Services act to tie multiple repo's together and present simplified objects back to the controllers, and ultimately the views.
However I can't come up with a clean pattern to associate eloquent models to each other at the service or repo layer.
Given the Post model has a belongsTo(User::class) relationship, how do I cleanly create that relationship at the Post repository layer.
I have tried:
public function associate($authorId)
{
$post->author()->associate($authorId);
}
But associate expects a user eloquent object, not just an id. I could do:
public function associate($authorId)
{
$post->from()->associate($userRepo->findEloquent($authorId));
}
But I feel like I am surfacing a eloquent model up into a repo that shouldn't be acting on it.
The easy way:
public function assignToAuthor($postId, $authorId)
{
$post = $this->find($postId); // or whatever method you use to find by id
$post->author_id = $authorId;
}
Now, the above implies that you know the foreign key author_id of the relation. In order to abstract it just a bit, use this:
public function assignToAuthor($postId, $authorId)
{
$post = $this->find($postId);
$foreignKey = $post->author()->getForeignKey();
$post->{$foreignKey} = $authorId;
}
Mind, that you still need to save the $post model, but I suppose you already know that.
Depending on your implementation of the simple, immutable, data object that you use, you could also allow passing the objects instead of raw ids. Something between the lines:
public function assignToAuthor($postId, $authorId)
{
if ($postId instanceof YourDataOject) {
$postId = $postId->getId();
}
if ($authorId instanceof YourDataOject) {
$authorId = $authorId->getId();
}
// ...
}
What I've done in the past that has brought some sanity to this situation for me was do things similar to what you are doing in your second associate method and prefix the repository with Eloquent so in the event I use something besides Eloquent, I just create a new implementation of the repository.
So in this case, I'd end up with class EloquentUserRepository implements UserInterface. I usually end up with some public methods which take and return only primitives and possibly some private methods which would be coupled to Eloquent so what I end up doing then is dropping those public methods into a AbstractUserRepository, or a trait if it makes more sense, to keep the code DRY.
It really depends on the situation, I had many thoughts on those actions as well on my repositories.
What I would suggest is to simply not use the "associate" function, you can simply do:
$post->user_id = $userID;
$post->save();
** of course you need to make sure that the user with that id exists.
A) You can do it outside with a special service for "associatingUser"
B) You can do it like you did with using the UserRepositoryInterface,
I see no problem adding the interface as a dependency.
Option A:
class AssociateUserToPost {
private $userRepo;
private $postRepo;
public function __construct(UserRepoInterface $userRepo, PostRepoInterface $postRepo) {
$this->userRepo = $userRepo;
$this->postRepo = $postRepo;
}
public function associate($userId, $postId) {
$user = $this->userRepo->getUser($userId);
if ( ! $user )
throw new UserNotExistException();
$post = $this->postRepo->getPost($postId);
if ( ! $post )
throw new PostNotExistException();
$this->postRepo->AttachUserToPost($postId, $userId);
}
}
option B (quite the same, code just sits in different places)
class PostRepository implements PostRepoInterface {
private $userRepo;
public function __construct(UserRepoInterface $userRepo) {
$this->userRepo = $userRepo;
}
public function associate($userId, $postId) {
$user = $this->userRepo->getUser($userId);
if ( ! $user )
throw new UserNotExistException();
$post = $this->getPost($postId);
if ( ! $post )
throw new PostNotExistException();
$this->AttachUserToPost($postId, $userId);
}
}
Hydration!
I'm assuming that another reason calling findEloquent within the post service seems icky is because you may have already retrieved that data within the controller. Simply put, you can access the same method that Eloquent uses to transform raw query results into fully functioning models.
$userData = array(
// simple, immutable data
);
$userCollection = User::hydrate(array($userData));
$userModel = $userCollection->first();
I think you actually need an additional layer, is what I call a Manager. This will contain all the business logic and will work only with interfaces. Under the hood it will call the services(each knowing to work with a specific resource/model)

Eager loading related model using query scope

Say I have a model Box which holds many widgets. The widgets can be active or inactive (boolean). The Widget model has a query scope which can filter results:
models/box.php:
class Box extends Eloquent
{
public function widgets()
{
return $this->hasMany('Widget');
}
}
models/widget.php:
class Widget extends Eloquent {
public function box()
{
return $this->belongsTo('Box');
}
public function scopeActive($query)
{
return $query->whereActive(true);
}
}
Query scopes make it easy to get all widgets for a given box:
$box_widgets = Box::find($box_id)->widgets()->active()->get();
// returns an Eloquent\Collection containing a filtered array of widgets
But how can I use scopeActive to eliminate this eager loading with method's conditional function?
$boxes = Box::with(array('widgets', function ($q)
{
$q->active();
}))->get();
It seems like there's probably a shorthand for accessing a relation's scope, something like Box::with('widgets->active') or Box::with('widgets.active') but I haven't been able to find it.
Suppose most of the time you want only active widgets, so I suggest:
public function widgets()
{
return $this->hasMany('Widget')->whereActive(true);
}
public function widgetsDisabled()
{
return $this->hasMany('Widget')->whereActive(false);
}
You can setup up more, for example for loading all at once, like you have now.
Then eager load as easily as that:
Box::with('widgets')... // loads only active

Resources