Model access in Laravel > 5 - php

First of all, I'm new to Laravel. I come from Codeigniter where you can have something similar to:
class Test_Model extends CI_Model {
public function test_method($a, $b){
return $a * $b;
}
}
class Test_Controller extends CI_Controller {
public function __construct(){
$this->load->model('Test');
}
public function method1() {
$z = $this->Test->test_method(3,4);
}
}
As you can see, the model was loaded and all it's methods were available. In my opinion it's pretty straightforward.
Now, I've got the following in Laravel:
namespace App;
use Illuminate\Database\Eloquent\Model;
// Order Model
class Order extends Model
{
protected $fillable = ['user_id'];
public function orderItems()
{
return $this->hasMany(orderItem::class);
}
}
// orderItem model
namespace App;
use Illuminate\Database\Eloquent\Model;
class orderItem extends Model
{
protected $fillable = [
'order_id',
'item_id',
'type',
'quantity',
'price',
'subtotal'
];
public function order()
{
return $this->belongsTo(Order::class);
}
}
// Orders Controller
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Order;
use App\orderItem;
class OrdersController extends Controller
{
private $orderId;
public function store(Request $request)
{
// check if there's already a cart [order] for this user, if not create one
$this->orderId = Order::where('user_id', $request->json('user_id'))->get(['id']);
$item = [
'item_id' => $request->json('item_id'),
'type' => $request->json('type'),
'quantity' => $request->json('quantity'),
'price' => $request->json('price'),
];
if (!$this->orderId->count()){
$this->orderId = Order::insertGetId([
'user_id' => $request->json('user_id')
]);
}
$orderItem = new Order();
$orderItem->addOrderItem($item, $this->orderId);
}
}
Two questions I've got:
Is there a simpler or cleaner (not saying that creating a new obj is not clean) to access the Order model methods (such as done in Codeigniter)?
I've learnt a little bit about how to establish relationships between models in Laravel. I've got two other tables name Record and Artist respectively (a 1 to many relationship) and I can do the following:
$record = Record::findOrFail($id);
$record['artist_name'] = $record->artist->name;
but when I try to do the same with the Order and orderItem (also a 1 to many relationship) it doesn't work:
$cart = Order::where('user_id', $user->id)->get();
// Retrieve existing items in cart
$cart_items = $cart->orderItems();
Why is that?

As for question 1:
If the method has nothing to do with the actual instance of the model I would strongly recommend not putting it on the model. You could create a separate class that doesn't extend Model class as there is no need to.
If you really want to, you could create a static method though.
If it does depend on the model (database row), there's no way of not instantiating it as it will need to know which database row to work on.
As for question 2:
This part $cart_items = $cart->orderItems(); only returns a query builder (as you're calling it as a function and not a property). Which lets you chain other query builder methods off of it.
For example $cart_items = $cart->orderItems()->get(); will return the actual order items.
Or you could just call it as a property and get the same result:
$cart_items = $cart->orderItems;
While the above should work, it is generally suggested that you eager load the relationships (especially when you're pulling multiple rows of the parent model), which would look like this (the ->with() part will eager load them):
$cart = Order::where('user_id', $user->id)->with('orderItems')->get();
$cart_items = $cart->orderItems;

Related

Laravel 5.7 - Eager loading with morphMany relationship and custom attribute getter

So I have the following models:
class TemplateEntity extends Model {
protected $table = "TemplateEntities";
const UPDATED_AT = null;
const CREATED_AT = null;
public function element() {
return $this->morphTo("element", "entity_type", "id_Entity");
}
public function getEntityTypeAttribute($entity_type) {
return 'App\\' . $entity_type;
}
}
class Template extends Model {
protected $table = "Template";
const UPDATED_AT = null;
const CREATED_AT = null;
public function entities() {
return $this->hasMany("App\TemplateEntity", "id_Template");
}
}
class TemplateEntity extends Model {
protected $table = "TemplateEntities";
const UPDATED_AT = null;
const CREATED_AT = null;
public function element() {
return $this->morphTo("element", "entity_type", "id_Entity");
}
public function getEntityTypeAttribute($entity_type) {
return 'App\\' . $entity_type;
}
}
I want to eager load template entity elements using Eloquent ORM's ::with() method, however whenever I do this I get an error:
//$template_id is defined as a controller param
$template = Template::with("entities", "entities.element")->where("id", "=", $template_id)->get()
"Class 'App\' not found"
I did some debugging and when I echo $entity_type in TemplateEntity's GetEntityTypeAttribute() method I get an empty value. However, my models generally work fine if I don't use eager loading, but I would like to add it to my application if possible to make it more efficient.
Any help you all can provide would help!
edit: fixed a typo, should have been Template::with instead of $template::with
Part of the problem might be a blank class in that variable. Suggest you use the class name when calling get(). So \App\Template:: instead of $template::.
Another item to help may be the way you are calling the relationship's eager load. Perhaps try to call through the function. This might work better for you:
\App\Template::with(['entities' => function($query){
$query->with('element');
}])->get();
The accessor function might be interfering with the Laravel morph function. I realise you want to use the shortened name of the class in the DB. To do this without the use of the getter (and globally), I suggest using a morphMap.
In AppServiceProvider inside the boot() method:
\Illuminate\Database\Eloquent\Relations\Relation::morphMap([
'MyTemplate' => \App\MyTemplate::class,
'Section' => \App\Section::class,
// etc.
]);
This will allow you to add only 'Section' to the DB and remove the accessor function from your class.

How can I compact this code for sending info to the view from the Laravel Controller?

Here's my edit function in the controller
public function edit($id)
{
$game = Game::find($id);
// build list of team names and ids
$allTeams = Team::all();
$team = [];
foreach ($allTeams as $t)
$team[$t->id] = $t->name();
// build a list of competitions
$allCompetitions = Competition::all();
$competition = [];
foreach ($allCompetitions as $c)
$competition[$c->id] = $c->fullname();
return View::make('games.edit', compact('game', 'team', 'competition'));
}
I am sending data in order to display in a select list. I know about Eloquent ORM method Lists, but the problem is as far as I know it can only take property names as an argument, and not methods (like name() and fullname()).
How can I optimize this, can I still use Eloquent?
I would look into attributes and appends. You can do what you would like by adjusting your models.
Competition
<?php namespace App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Competition extends Model
{
protected $appends = ['fullname'];
...
public function getFullnameAttribute()
{
return $this->name.' '.$this->venue;
}
}
Team
<?php namespace App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
protected $appends = ['name'];
...
public function getNameAttribute()
{
return $this->city.' '.$this->teamName;
}
}
Controller
public function edit($id)
{
$game = Game::find($id);
$team = Team::get()->lists('id','name');
$competition = Competition::get()->lists('id','fullname');
return View::make('games.edit', compact('game', 'team', 'competition'));
}
The only thing I can think of (aside from using the map functionality of Eloquent collections) is to overwrite the toArray method in your model to add some custom attributes.
Eg.
public function toArray()
{
return array_merge(parent::toArray(), [
'fullname' => $this->fullname(),
]);
}
This will allow you to use something like:
$competition = $allCompetitions->fetch('fullname');
Although:
In saying all this I think the more elegant solution is to just provide the whole competition objects to the view and let the loop where you render them (or whatever) call the method itself.
You can call model method in view file if they are not related with other models. So if name() & fullname() returns result related to this model then you can use this model methods in view
#foreach (($allTeams as $t)
{{ $t->name() }}
#endforeach
ofcourse you have to pass the $allteams collection from controller to view

Laravel - Using controller function for two different eloquent models

I am currently developing an application for an indy movie production company. The way I have the workflow right now, the user begins by creating a new movie object by entering the movie title and synopsis. From there the user can then add more details such as price, run-time, full-screen/wide-screen, etc. The movie basic (title, synopsis) are in one database table, and the details are in another. I have set up a one-to-one relationship between the two eloquent models. I have also set up a MovieController that allows me to very easily do CRUD operations on the movie basic model, and when I am displaying the movie object to the user, I can display both the basics and details.
What I was wondering was there some way to use the already existent functions in the movie controller to do CRUD operations on the movie details without having to create new functions in the controller? Also is it possible to reuse the views I've created for each corresponding CRUD operation? In other words can I would like
something.dev/cms/create
In one instance to match to creating a new movie (title, synopsis) and in another instance to match to creating the movie detail (price, run-time, full-screen/widescreen) etc. Is this possible? I have provide the code for the two models below:
Movie_basic.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_basic extends Model {
protected $fillable = ['movie_title', 'movie_synopsis'];
protected $guarded = ['id'];
public function details()
{
return $this->hasOne('App\Movie_detail', 'movie_id');
}
public function personnel()
{
return $this->hasMany('App\Movie_personnel', 'movie_id');
}
}
Model_detail.php
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Movie_detail extends Model {
protected $fillable = ['minutes', 'languages', 'viewer_discretion', 'screen_type', 'price'];
protected $guarded = ['id', 'movie_id'];
public function basics()
{
return $this->belongsTo('App\Movie_basic');
}
}
If I understand you, this might be an answer. (Did not test the code.)
Please note that, that code has been written to show you an example. You will probably want to edit it to make it work and act as you wanted. Maybe you want to use a repository or automate the model instance creating (I did not create new instances), and saving processes. You can use interfaces instead of your models etc...
Here is the service to store the logic.
<?php
use Movie_basic; use Movie_detail;
Class MovieService {
protected $movieBasic;
protected $movieDetail;
public function __construct(Movie_basic $movieBasic, Movie_detail $movieDetail) {
$this->movieBasic = $movieBasic;
$this->movieDetail = $movieDetail;
}
public function createMovie(array $attr) {
// TODO: Move your business logic here.
// E.g
$movie = $this->movieBasic->fill($attr);
$movie->save();
return $movie;
}
public function createMovieDetail(array $movieAttr, array $attributes) {
// TODO: Move your detail logic here.
// E.g.
$basic = $this->createMovie($movieAttr);
$detail = $this->movieDetail->fill($attributes);
$detail->basic()->associate($detail);
$detail->save();
return $detail;
}
}
And here, the controller examples:
<?php
use MovieService;
class MovieController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovie($attrToSave);
}
}
<?php
use MovieService;
class MovieDetailController {
public function __construct(MovieService $ms) {
$this->ms = $ms;
}
public function store() {
$this->ms->createMovieDetail($attrToSave);
}
}

Updating timestamps on attaching/detaching Eloquent relations

I'm using Laravel 4, and have 2 models:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product');
}
}
class Product extends \Eloquent {
public function assets() {
return $this->belongsToMany('Asset');
}
}
Product has the standard timestamps on it (created_at, updated_at) and I'd like to update the updated_at field of the Product when I attach/detach an Asset.
I tried this on the Asset model:
class Asset extends \Eloquent {
public function products() {
return $this->belongsToMany('Product')->withTimestamps();
}
}
...but that did nothing at all (apparently). Edit: apparently this is for updating timestamps on the pivot table, not for updating them on the relation's own table (ie. updates assets_products.updated_at, not products.updated_at).
I then tried this on the Asset model:
class Asset extends \Eloquent {
protected $touches = [ 'products' ];
public function products() {
return $this->belongsToMany('Product');
}
}
...which works, but then breaks my seed which calls Asset::create([ ... ]); because apparently Laravel tries to call ->touchOwners() on the relation without checking if it's null:
PHP Fatal error: Call to undefined method Illuminate\Database\Eloquent\Collection::touchOwners() in /projectdir/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 1583
The code I'm using to add/remove Assets is this:
Product::find( $validId )->assets()->attach( $anotherValidId );
Product::find( $validId )->assets()->detach( $anotherValidId );
Where am I going wrong?
You can do it manually using touch method:
$product = Product::find($validId);
$product->assets()->attach($anotherValidId);
$product->touch();
But if you don't want to do it manually each time you can simplify this creating method in your Product model this way:
public function attachAsset($id)
{
$this->assets()->attach($id);
$this->touch();
}
And now you can use it this way:
Product::find($validId)->attachAsset($anotherValidId);
The same you can of course do for detach action.
And I noticed you have one relation belongsToMany and the other hasMany - it should be rather belongsToMany in both because it's many to many relationship
EDIT
If you would like to use it in many models, you could create trait or create another base class that extends Eloquent with the following method:
public function attach($id, $relationship = null)
{
$relationship = $relationship ?: $this->relationship;
$this->{$relationship}()->attach($id);
$this->touch();
}
Now, if you need this functionality you just need to extend from another base class (or use trait), and now you can add to your Product class one extra property:
private $relationship = 'assets';
Now you could use:
Product::find($validId)->attach($anotherValidId);
or
Product::find($validId)->attach($anotherValidId, 'assets');
if you need to attach data with updating updated_at field. The same of course you need to repeat for detaching.
From the code source, you need to set $touch to false when creating a new instance of the related model:
Asset::create(array(),array(),false);
or use:
$asset = new Asset;
// ...
$asset->setTouchedRelations([]);
$asset->save();
Solution:
Create a BaseModel that extends Eloquent, making a simple adjustment to the create method:
BaseModel.php:
class BaseModel extends Eloquent {
/**
* Save a new model and return the instance, passing along the
* $options array to specify the behavior of 'timestamps' and 'touch'
*
* #param array $attributes
* #param array $options
* #return static
*/
public static function create(array $attributes, array $options = array())
{
$model = new static($attributes);
$model->save($options);
return $model;
}
}
Have your Asset and Product models (and others, if desired) extend BaseModel rather than Eloquent, and set the $touches attribute:
Asset.php (and other models):
class Asset extends BaseModel {
protected $touches = [ 'products' ];
...
In your seeders, set the 2nd parameter of create to an array which specifies 'touch' as false:
Asset::create([...],['touch' => false])
Explanation:
Eloquent's save() method accepts an (optional) array of options, in which you can specify two flags: 'timestamps' and 'touch'. If touch is set to false, then Eloquent will do no touching of related models, regardless of any $touches attributes you've specified on your models. This is all built-in behavior for Eloquent's save() method.
The problem is that Eloquent's create() method doesn't accept any options to pass along to save(). By extending Eloquent (with a BaseModel) to accept the $options array as the 2nd attribute, and pass it along to save(), you can now use those two options when you call create() on all your models which extend BaseModel.
Note that the $options array is optional, so doing this won't break any other calls to create() you might have in your code.

Get array of Eloquent model's relations

I'm trying to get an array of all of my model's associations. I have the following model:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public function author()
{
return $this->belongsTo('Author');
}
public function category()
{
return $this->belongsTo('Category');
}
}
From this model, I'm trying to get the following array of its relations:
array(
'author',
'category'
)
I'm looking for a way to pull this array out from the model automatically.
I've found this definition of a relationsToArray method on an Eloquent model, which appears to return an array of the model's relations. It seems to use the $this->relations attribute of the Eloquent model. However, this method returns an empty array, and the relations attribute is an empty array, despite having my relations set up correctly.
What is $this->relations used for if not to store model relations? Is there any way that I can get an array of my model's relations automatically?
It's not possible because relationships are loaded only when requested either by using with (for eager loading) or using relationship public method defined in the model, for example, if a Author model is created with following relationship
public function articles() {
return $this->hasMany('Article');
}
When you call this method like:
$author = Author::find(1);
$author->articles; // <-- this will load related article models as a collection
Also, as I said with, when you use something like this:
$article = Article::with('author')->get(1);
In this case, the first article (with id 1) will be loaded with it's related model Author and you can use
$article->author->name; // to access the name field from related/loaded author model
So, it's not possible to get the relations magically without using appropriate method for loading of relationships but once you load the relationship (related models) then you may use something like this to get the relations:
$article = Article::with(['category', 'author'])->first();
$article->getRelations(); // get all the related models
$article->getRelation('author'); // to get only related author model
To convert them to an array you may use toArray() method like:
dd($article->getRelations()->toArray()); // dump and die as array
The relationsToArray() method works on a model which is loaded with it's related models. This method converts related models to array form where toArray() method converts all the data of a model (with relationship) to array, here is the source code:
public function toArray()
{
$attributes = $this->attributesToArray();
return array_merge($attributes, $this->relationsToArray());
}
It merges model attributes and it's related model's attributes after converting to array then returns it.
use this:
class Article extends Eloquent
{
protected $guarded = array();
public static $rules = array();
public $relationships = array('Author', 'Category');
public function author() {
return $this->belongsTo('Author');
}
public function category() {
return $this->belongsTo('Category');
}
}
So outside the class you can do something like this:
public function articleWithAllRelationships()
{
$article = new Article;
$relationships = $article->relationships;
$article = $article->with($relationships)->first();
}
GruBhub, thank you very much for your comments. I have corrected the typo that you mentioned.
You are right, it is dangerous to run unknown methods, hence I added a rollback after such execution.
Many thanks also to phildawson from laracasts, https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships
You can use the following trait:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Relations\Relation;
trait EloquentRelationshipTrait
{
/**
* Get eloquent relationships
*
* #return array
*/
public static function getRelationships()
{
$instance = new static;
// Get public methods declared without parameters and non inherited
$class = get_class($instance);
$allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC);
$methods = array_filter(
$allMethods,
function ($method) use ($class) {
return $method->class === $class
&& !$method->getParameters() // relationships have no parameters
&& $method->getName() !== 'getRelationships'; // prevent infinite recursion
}
);
\DB::beginTransaction();
$relations = [];
foreach ($methods as $method) {
try {
$methodName = $method->getName();
$methodReturn = $instance->$methodName();
if (!$methodReturn instanceof Relation) {
continue;
}
} catch (\Throwable $th) {
continue;
}
$type = (new \ReflectionClass($methodReturn))->getShortName();
$model = get_class($methodReturn->getRelated());
$relations[$methodName] = [$type, $model];
}
\DB::rollBack();
return $relations;
}
}
Then you can implement it in any model.
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
use App\Traits\EloquentRelationshipTrait;
class User extends Authenticatable
{
use Notifiable, HasApiTokens, EloquentRelationshipTrait;
Finally with (new User)->getRelationships() or User::getRelationships() you will get:
[
"notifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"readNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"unreadNotifications" => [
"MorphMany",
"Illuminate\Notifications\DatabaseNotification",
],
"clients" => [
"HasMany",
"Laravel\Passport\Client",
],
"tokens" => [
"HasMany",
"Laravel\Passport\Token",
],
]
I have published a package in order to get all eloquent relationships from a model. Such package contains the helper "rel" to do so.
Just run (Composer 2.x is required!):
require pablo-merener/eloquent-relationships
If you are on laravel 9, you are able to run artisan command model:show

Categories