Modeling an Object with one-to-many relationships (PHP) - php

I am trying to force myself to be consistent when modeling objects, but I'm just not sure what the best way is to create a class that has one-to-many relationships.
For example, lets say I have two tables in a database called Projects & Notes. A note can only belong to one project and a project can have many notes. Would the following be a good (best) way to model my project class? Or should the notes just be set to a separate variable in the controller and never be a property of project?
class Project extends BaseModel{
$id //string
$description //string
$notes //array of note objects
}
class Note extends BaseModel{
$id //string
$text//string
}
//In a Controller Class
$project = new Project();
$noteArray = new Note();
//Set the note property of project equal to an array of note objects
$project->setNotes($noteArray->findNotes($project->id));

I think there should be one more property in Note model that will reference to the Project model. Identificators of model MUST be an integer type
class Note extends BaseModel{
$id //string
$text//string
$project_id //int
}
So when you add a project, say it, with ID=5, You can add Notes with project_id = 5. And it will be one-to-many relatoionship.

Related

Can I cast one Eloquent model to another?

In eloquent you can have different models operating on a same table.
So, is it possible to switch an object from one Model to another in a runtime.
Assuming that we have three classes:
Product extends Model
ProductSet extends Product
SimpleProduct extends Product
The difference between ProductSet and a SimpleProduct is column complex which has 'true' in it for a ProductSet and 'false' for a SimpleProduct.
ProductSet and a SimpleProduct models have Scopes applied to them so that ProductSet::all() will get only sets, and SimpleProduct::all() will get only a simple products.
Parent model however allow us to get both types of products via Product::all()
so, is it possible to go through all instances of Product class and switch them to their higher level models at runtime based on a value of property complex ?
Since you're already extending the class which holds the same props as the parent. You can loop over any product collection and get all the attributes through getAttributes() method and pass to the parent. Because each of your model is extending Eloquent Model which accepts attribute in the constructor and fills its attribute.
Following example extends the default User class and converts back to the Parent class
<?php
namespace App;
class Admin extends User {}
Get all Admins and convert them to User
$admins = \App\Admin::all();
$users = [];
foreach ($admins as $admin) {
$users[] = new \App\User($admin->getAttributes());
}
dd(collect($users));
Yes you can do that, Based on the prop you have to create object of that specific class

Laravel models: Where are model properties?

(I come from Visual Studio + Entity Framework background and trying to locate equivalent functionality in Laravel + Eloquent)
In EF and Visual Studio, we add a new Model to our application and just tell it about our existing database. EF can then generate Models for my tables with public properties for columns. This gives us all those IDE and compiler benefits such as Intellisense, spelling-error detection etc.
I've recently stated exploring VS Code, Laravel and Eloquent. Going through all those tutorials and articles, I'm not sure when and how these properties are generated in the model classes. I just tried artisan make:model command and it did generate the model class, but there are no properties in it. So,
Am I supposed to write them by hand? (really?)
Will these just be public variables or standard properties with getter/setter (excuse me for my .NET mentality :))?
Is there a tool/extension that could examine my database and create models with properties for their columns?
Update
To the people who answered my question, thanks a lot. Plus some of the comments I posted were due to my ignorance about PHP's (strange IMO) approach about member access; I just found out that PHP does not complain about non-existing class members and instead generates them on the fly (e.g. $post->NonExistingMember = SomeValue runs okay; this would not even compile in most other languages that I know). Big surprise for me. I have used C++, VB, C#, Java among several other languages and haven't seen that behavior anywhere else. All those languages would throw a compile-time error straight away saying something like Type X does not contain a member named Y. Cannot see how PHP's different approach fits together with OOP.
The actual problem that I posted this question for still remains unresolved. Although I can use reliese/laravel to generate Model classes for my database, the tool still does not generate class members against table columns, so I do not get auto-complete benefits. I'd love to hear from the experts if that can be done (automatically of course).
Update 2
Now that I understand Laravel environment slightly better, I thought I'd share my experience. See my answer below.
Now that I have spent some time with Laravel, Eloquent and PHP in general, I'll share a few things in the hope that these helps other starters.
PHP is a dynamic language and its code is compiled on the fly (this is unlike C# and VB.NET). Your model classes do not need to explicitly define members for them to be accessible/assignable, so as long they extend Model (a built-in class in Laravel Eloquent), you can assign values to non-existing members that have the same name as the underlying database table column and Eloquent will store it in the DB for you. So for example, if you have a posts table in your database that has a column named body, you can write the following code to create a new record in your database:
$p = new Post;
$p->body = 'Some stuff';
$p->save();
Of course you need to have a class Post in your project that extends from Model but you don't need to define a member body inside that class. This would sound strange to the people coming from .NET world, but that's how dynamic languages work.
As for automatically generating models, Laravel includes built-in commands (php artisan make:model) that can generate those for you.
Lastly, for intellisense and auto-complete, use the same tool that is used by Laravel itself, i.e. DocBlocks. These are special type of comments in PHP using which you can document your code elements. So you can add DocBlocks to all your model classes containing property names and types. Fortunately for everyone, there is a very neat extension in VS Code that can do this automatically for you. Install it using the following command:
composer require --dev barryvdh/laravel-ide-helper
Now run the following command to generate DocBlocks for all of your model classes (obviously you should already have generated your database and models before this):
php artisan ide-helper:models --dir='app'
The extension will fetch the structure of your database and inject DocBlocks to all your models, which will look something like this:
/**
* App\User
*
* #property int $id
* #property string $name
* #property \Illuminate\Support\Carbon|null $created_at
* #property \Illuminate\Support\Carbon|null $updated_at
* #method static \Illuminate\Database\Eloquent\Builder|\App\User whereCreatedAt($value)
* #method static \Illuminate\Database\Eloquent\Builder|\App\User whereId($value)
* #method static \Illuminate\Database\Eloquent\Builder|\App\User whereName($value)
* #method static \Illuminate\Database\Eloquent\Builder|\App\Exam whereUpdatedAt($value)
* #mixin \Eloquent
*/
class User extends Model
{
}
VS Code will now show you table field names in model properties, like this (see how intellisense brings up name member from our DocBlocks as we type na...):
Note that I also have Intelephense installed in my VS Code, though I'm not sure if that is required for auto-complete feature to work.
Edit
Dynamic Properties have been deprecated in PHP 8.2 and I'm hearing that they'll become invalid in PHP 9.0, which means Laravel models should not be able to do this magic stuff in the future versions.
I'm not a PHP guru, but I hear that we don't need to panic. Two things: Firstly, the objects implementing __get and __set will keep working fine. Secondly, Plus you (they) can also use #[AllowDynamicProperties] on model classes to allow dynamic props. And lastly, they can rewrite model generator to spit out column names as props in the model class. This last one will be the best and will take PHP one step closer to how C# world works (precisely where this post started, lol).
I use annotations to declare all the properties for autocomplete (works in PHPStorm, not sure about other IDEs).
/**
* #property $id
* #property $microsoft_id
* #property $name
* #property $qualification
* #property $company
*/
class ShopTenant extends Model
{
public $connection = 'shop';
public $table = 'tenants';
protected $guarded = ['id'];
}
You can use something like this to get a list of all columns of a table, then paste/format that list into your annotations:
SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = N'tenants';
Unfortunatilly yes, you need to write them by hand, but only if you need to update the value of these properties, check point 2($fillable array)
You need to declare the properties that can be filled:
For example a model called Post for a database table "posts" that has 2 columns "title" and "body" :
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
//not mandatory to declare the table name, Laravel will find it using reflection if you follow the naming standard
protected $table = 'posts'; //not mandatory
protected $fillable = ['title','body'];
}
In the controller:
$newPost = new Post;
$newPost->title = 'A new title';
$newPost->body = 'A new body';
$newPost->save(); //only at this point the db is modified
Or you can hide them if you return the properties in an array or a JSON response(in the Collection also the hidden ones will be displayed):
protected $hidden = [
'title',
];
Also you can inject new properties in the model using Accessors
I don't think so, you can install some Laravel VS Code plugins to make your life easier(e.g: Blade snippets)
You can check this article.
Actually you don't need to specify the properties. Its all done by laravel automatically. When you create a model laravel uses the plural (and it has a really good system for that: post becomes posts, activity becomes activities, etc.) of the classname to access the table. So you can work with an empty model without setting the $table or the $fillable/$guarded property.
// use only, if your table name differs from the models classname
$table = 'users_options'; // when you want to name your model 'Vote' but the table is called 'users_options' for instance.
// use fillable and guarded only to specify mass-assignment columns
$fillable = [whatever, ...];
$guarded = [whatever, ...];
you can access the properties whenever you want:
class Post extends Model
{
}
// would do sth like this: select name from posts where id = 1;
// without specifying anything in the model
$name = Post::find(1)->name;
You need to add by yourself. for example
$fillable = ['firstname', 'email','username'];
$guarded = ['price'];

Many-To-Many relationship save fails

I have this Many-To-Many relation:
These are my Models:
Productvariant
class Productvariant extends DataMapper {
var $has_many = array('propertyvalue');
var $has_one = array('product');
}
Propertyvalue
class Propertyvalue extends DataMapper {
var $has_many = array('productvariant');
var $has_one = array('property');
}
Controller
$productvariant = new Productvariant(1);
$prodval = new Propertyvalue(1);
$productvariant->save($prodval);
Message
Unable to relate productvariant with propertyvalue.
Only thing I can find in the documentation is self Many-To-Many relationship and I seem to misread how they want you to use the models in that fashion.
Do I need to define a model for the extra table too?
==========================
UPDATE
I made an in-between Model for Many-To-Many relationship;
Model
class Productvariant_propertyvalues extends DataMapper {
var $table = '__productvariants_propertyvalues';
var $has_one = array('productvariant', 'propertyvalue');
}
Controller
$productvariant = new Productvariant(1);
$propval = new Propertyvalue(1);
$pv_vals = new Productvariant_propertyvalues();
$pv_vals->save(array($productvariant, $propval));
It works now, but shouldn't this be doable without the extra Model?
You either have a many-to-many between Productvariant and Propertyvalue, and then you define them as you did. Datamapper will automatically look for a junction table called "productvariants_propertyvalues", which contains the foreign keys to the two tables.
So your setup should work. They only issue that could pop up is that the CI plural() function that datamapper uses doesn't produce the correct table name, causing the not-found error to popup.
If you create an in-between model, you split the many-to-many into two one-to-many's. Not a problem, but whether or not you need it depends on your application design.
edit: I now see you have prefixed the table name with a double underscore. Datamapper will not generate that, so that is why the junction table can not be found. Either remove those, or switch to an advanced relationship definition in which you define the "join_table" manually.

PHP Pattern - how to retrieve collection of objects

Situation:
There is simple User class which is Doctrine entity (I skipped comments to keep the code short):
class User {
protected $iUserId;
protected $sName;
}
Question:
How to retrieve collection of objects of class User from the controller?
Follow up:
Until now, we were creating methods like getUsers() in User class which gets data from DB, creates User class objects and returns it.
Now, I'm wondering if it's not a better solution to create class like UserCollection which would handle data retrieving and creating User objects? Maybe I should make use of \Doctrine\Common\Collections\Collection class somehow?
What I'd like to accomplish is easy way to handles i.e. where clauses. UserCollection class could have QueryBuilder object on which I could operate from controller like this:
$oUserCollection = new UserCollection();
$oUserCollection->setWhere( 'u.iUserId = 1' );
$aUsers = oUserCollection->getUsers();
...
Please share your thoughts on that topic.
Update:
Doctrine provides a concept of repositories of entities which I think maybe the solution I'm looking for.
You have two options:
If your criterias are simple, eg. you just want to filter on a single property, or you don't need filtering at all, you can use the findBy() and findAll() methods, on the EntityRepository:
// find all users
$repository = $em->getRepository('My\\User');
$users = $repository->findAll();
// find users who are marked as active
$users = $repository->findBy(array('active' => true));
// sort users by age
$users = $repository->findBy(array(), array('age' => 'DESC'));
If you have complex(er) requirements, or your finders will be used from multiple places, you will have to create a custom EntityRepository, and group your finder logic there. You have to specify in the mapping that you want to create your own EntityRepository for this entity, the method for this varies depending on what mapping driver you use (annotation, yaml, xml). For example, in yaml you would have to place this line in your mapping:
RepositoryClass: My\Repository\UserRepository
And then create the file:
namespace My\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository {
public function findVIPUsers() {
$query = $this->_em->createQuery("Your query here");
return $query->getResult();
// you can also use $this->findBy(), or $this->findAll() here
}
}
And then in your controller, when you call $em->getRepository('My\User') it will return the custom repository you just created.

Zend Db Table Abstract Manipulate select()

is there any chance to set a where statement on any select() request regarding a Zend Db Table Abstract Object?
Example:
We have 2 roles, 1 role 'admin' which is allowed to see all products and 1 role 'supplier' which is allowed to see only their own products.
I don't want to check the role and set the where statement for the Zend Db Table Abstract Object in every ActionController. Is there any chance to do this in the Zend Db Table Abstract?
class ProductsModel extends Zend_Db_Table_Abstract
{
protected $_name = 'artikel';
protected $_primary = 'ID';
protected $_where = ('supplier = ?', $this->_auth->Role ); # SOMETHING LIKE THAT ??
}
Thanks!
M.
There are two definitions in ZF: Model and Table. You should not substitute them. The model describes abstract layer between Controller and Table. The model may work with Table by means of Mapper where you can implement all business logic (where clauses, order, limit and so on).
It all are included into standart ZF example template.

Categories