How to access related record in CakePHP with mysql? - php

I have a database of two tables. One with blog posts and one with users, related by a user_id field in the post table. On my index page I have a table of the posts and I want to add the author to that however I want to display the user's name rather than their ID. I am trying to add an author field to my post objects like this in PostsController:
public function index() {
$this->set('posts', $this->Post->find('all'));
foreach ($this as $post){
$post['Post']['author'] = $this->User->findById($post['Post']['user_id']);
}
}
however this brings the error that I am calling findById on null. I am very new to php so I think my understanding of how to use the loop may be incorrect. Perhaps there is a better way which does not require the loop?

Controllers in CakePHP, by default, only load their own models. If you need an additional model at some point, you need to load it in manually.
That won't solve your problem though, because you're setting the result for the find() action straight into the view. You'll want to wait with that until you add the users to it. Oh, and you usually can't iterate through $this with foreach, unless your class implements an Iterator-like interface (which controllers should never have a reason to do)
public function index() {
// first load in the User model
$this->loadModel('User');
// store the posts in a local variable first
$posts = $this->Post->find('all');
// loop through the local variable, also keep the index so we can reference
// the post we're modifying
foreach ($posts as $index => $post) {
$post['Post']['author'] = $this->User->findById($post['Post']['user_id']);
// write the modified $post back into the $posts array
$posts[$index] = $post;
}
// **now** you can make $posts available to your view
$this->set('posts', $posts);
}
Once you have this sorted out, read up on linking models together. There's a way to set up your Post model so that it will automatically fill $post['Post']['author'] with the corresponding User, without you having to do that manually.

Better you specify the relation in model.
In Post Model initialize the relation between post and user
public $hasOne = 'User';
Now in controller use Contain() to get linked models data
$posts = $this->Post->find('all')->contain(['User']);
$this->set('posts', $posts);
You will get User object with each post record which you can use to get user name, you do not need to write a separate query to fetch the user name.

Related

Laravel - Get belongsTo relationship without id using Eloquent

i'm using laravel 7.30, I have a post model linked to user model with belongsTo relationship, I want to retrieve all posts with user property that contains only the name of the user for api purposes.
what i've tried so far.
public function index()
{
return Post::with('user:id,name')->get();
}
but the result of this code is a nested object 'user' which has 2 fields id and name on each post, I only want a single field with posts fields called user which has the name only and not a nested object for api purposes.
I've made it using database query builder, but i'm looking for a way using Eloquent
Thanks in advance
You can add this to your Post Model:
protected $appends = [
"author_name"
];
public function getAuthorNameAttribute()
{
return $this->user->name;
}
This will make sure the user's name is appended to the Post object every time it is being called.
Then from your controller, you can easily do this:
public function index()
{
return Post::get();
}

Get name instead of ID one to one relationship

I have a make table and post table. Make table saves make names as make_code and make_name.
Post table has a column make. While saving a post, it will save make in make_code.
While displaying in blade, I want it to display as make_name. How can I do it?
Currently {{$post->make}} gives me make_code. I need it to show make_name.
I think its a one-to-one relationship that's needed. I tried putting it in model but did not work. How can I achieve it?
MAKE MODEL
class Make extends Model
{
public function make()
{
return $this->belongsTo(App\Post::class);
}
}
POST MODEL:
class Post extends Model
{
protected $table = 'posts';
}
Update
As Tim Lewis noticed:
the relationships can't be named make, as that's a conflict.
Assuming that the your relationship work like this:
a Make has many Post
a Post belongs to a Make object.
| Note: Correct me if I'm wrong.
So, if this is correct, you should define your relationships like this:
Post.php
public function make_rel()
{
return $this->belongsTo(Make::class, 'make', 'make_code');
}
Make.php
public function posts()
{
return $this->hasMany(Post::class, 'make', 'make_code');
}
Check the One-to-Many and One-to-Many (Inverse) relationship sections of the documentation.
So, you could do in your controller (or wherever you want):
$post = Post::find(1);
dd($post->make_rel->make_name); // 'Harley Davidson'
Additionally, you could create a computed property as a shorcout to access this related property in your Post model:
Post.php
// ...
public function getMakeNameAttribute()
{
return $this->make_rel->make_name;
}
Now, you can access it like this:
$post = Post::find(1);
dd($post->make_name); // 'Harley Davidson'
Suggestion
As a suggestion, I strongly advice you to change your foreign key column from make to make_id (in your 'posts' table) to avoid conflicts. Also, you could relate the post to the make primmary key instead of a custom key given the fact that this link is almost invisible and it is handled by Laravel. This would speed up the execution of the query because primmary id's are indexed by default.

Advanced Laravel merged data/models - can it be done at model level?

We have a COMMON database and then tenant databases for each organization that uses our application. We have base values in the COMMON database for some tables e.g.
COMMON.widgets. Then in the tenant databases, IF a table called modified_widgets exists and has values, they are merged with the COMMON.widgets table.
Right now we are doing this in controllers along the lines of:
public function index(Request $request)
{
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->get();
$merged = $widgets->merge($modified);
$merged = array_values(array_sort($merged, function ($value) {
return $value['name'];
}));
return $merged;
}
return $countries;
}
As you can see, we have model for each table and this works OK. We get the expected results for GET requests like this from controllers, but we'd like to merge at the Laravel MODEL level if possible. That way id's are linked to the correct tables and such when populating forms with these values. The merge means the same id can exist in BOTH tables. We ALWAYS want to act on the merged data if any exists. So it seems like model level is the place for this, but we'll try any suggestions that help meet the need. Hope that all makes sense.
Can anyone help with this or does anyone have any ideas to try? We've played with overriding model constructors and such, but haven't quite been able to figure this out yet. Any thoughts are appreciated and TIA!
If you put this functionality in Widget model you will get 2x times of queries. You need to think about Widget as an instance, what I am trying to say is that current approach does 2 queries minimum and +1 if tenant has modified_widgets table. Now imagine you do this inside a model, each Widget instance will pull in, in a best case scenario its equivalent from different database, so for bunch of Widgets you will do 1 (->all())+n (n = number of ModifiedWidgets) queries - because each Widget instance will pull its own mirror if it exists, no eager load is possible.
You can improve your code with following:
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->whereIn('id', $widgets->pluck('id'))->get(); // remove whereIn if thats not the case
return $widgets->merge($modified)->unique()->sortBy('name');
}
return $widgets;
OK, here is what we came up with.
We now use a single model and the table names MUST be the same in both databases (setTable does not seem to work even though in exists in the Database/Eloquent/Model base source code - that may be why it's not documented). Anyway = just use a regular model and make sure the tables are identical (or at least the fields you are using are):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Widget extends Model
{
}
Then we have a generic 'merge controller' where the model and optional sort are passed in the request (we hard coded the 'where' and key here, but they could be made dynamic too). NOTE THIS WILL NOT WORK WITH STATIC METHODS THAT CREATE NEW INSTANCES such as $model::all() so you need to use $model->get() in that case:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MergeController extends Controller
{
public function index(Request $request)
{
//TODO: add some validations to ensure model is provided
$model = app("App\\Models\\{$request['model']}");
$sort = $request['sort'] ? $request['sort'] : 'id';
$src_collection = $model->where('active', '1')->orderBy('name')->get();
// we setup the tenants connection elsewhere, but use it here
if(Schema::connection('tenant')->hasTable($model->getTable())) {
$model->setConnection('tenant');
$tenant_collection = $model->get()->where('active', '1');
$src_collection = $src_collection->keyBy('id')->merge($tenant_collection->keyBy('id'))->sortBy('name');
}
return $src_collection;
}
}
If you dd($src_collection); before returning it it, you will see the connection is correct for each row (depending on data in the tables). If you update a row:
$test = $src_collection->find(2); // this is a row from the tenant db in our data
$test->name = 'Test';
$test->save();
$test2 = $src_collection->find(1); // this is a row from the tenant db in our data
$test2->name = 'Test2'; // this is a row from the COMMON db in our data
$test2->save();
dd($src_collection);
You will see the correct data is updated no matter which table the row(s) came from.
This results in each tenant being able to optionally override and/or add to base table data without effecting the base table data itself or other tenants while minimizing data duplication thus easing maintenance (obviously the table data and population is managed elsewhere just like any other table). If the tenant has no overrides then the base table data is returned. The merge and custom collection stuff have minimal documentation, so this took some time to figure out. Hope this helps someone else some day!

Calling Laravel relationship with aggregate

If I have a Laravel 5.5 model called User that hasMany Posts and each Post hasMany Hits, is there an aggregate function I can call to get the total number of Hits for a User across all Posts, where the Hit was created in the last week?
It seems like there may be a clever way to do it besides doing something like
$hits = $user->posts()->hits()
and then looping over those hits to check created date.
In this case it seems like raw sql would be better, but I figured there may be an Eloquent way to handle a situation like this.
I think the right solution is just to use a HasManyThrough relationship to grab all the Hit rows, joined through the posts table.
So it'd look like this on the User model (roughly):
return $this->hasManyThrough(
Hit::class,
Post::class
// if you have non-standard key names you can specify them here-- see docs
);
Then when you have your User model you can just call $user->hits to get a collection of all the associated hits through all the user's Posts
You can add the code below to your Post model.
protected static function boot()
{
parent::boot();
static::addGlobalScope('hitCount', function ($builder) {
$builder->withCount('hits');
});
}
It automatically provides a field hits_count whenever you fetch a post.
$post = Post::first();
$hits = $post->hits_count; //Count hits that belongs to this post
You can read the documentation here to customize it to your need.
Set HasManyThrough relation in the User model:
public function hits()
{
return $this->hasManyThrough('App\Models\Hits','App\Models\Posts','user_id','post_id','id');
}
then you can do this:
$reults = $user->hits()->where('hits_table_name.created_at', '>=', Carbon::today()->subWeek())->count();
HasManyThrough Link
Use DB::enableQueryLog(); and DB::getQueryLog(); to see if executed SQL Query is correct;

Trying to get data via related table in Laravel

What I'm trying to do is to show the posts that have been saved by the user in the profile. I will try to explain it as good as possible refering to my code. So:
public function userProfil($id)
I have the profile function which get the data from userprofile table. and inside I have the following code for saved data:
$authed = User::find($id);
$savedarticles = $authed->mysaves;
$allsavings = DB::select("Select * from article where id=$savedarticles->id");
But this code does not work like this anyway. I can do this instead:
$authed = User::find($id);
$savedarticles = $authed->mysaves;
But when I try to get articles from article table with the article_id of mysaves, it does not work such as this:
$allsaved= DB::table('article')->where('id', $savedarticles->article_id);
the error it gives is like:
Property [article_id] does not exist on this collection instance.
although savearticle table has article_id I can output it without the line above and in view I get them as:
#foreach($savedarticles as $savedarticle)
<p>{{$savedarticle}}</p>
#endforeach
it gives me everything that is in the savearticle table and I can get do savedarticle->article_id and get article_id but can't get it in controller.
I am using Laravel 5.4.
The error message Property [article_id] does not exist on this collection instance. means you are trying to get an attribute of a single instance but from a collection.
For example the collection could be like
[$article1, $article2, $article3]
therefore what you tried to do is something similar to
[$article1, $article2, $article3]->article_id
You are trying to get an attribute from a collection instead of a single instance.
As for your query, you can use where in sql statement to search for rows that match any item in an array
$allsaved= DB::table('article')->whereIn('id', $savedarticles->pluck('article_id')->all());
What I have understood is that A USER has many POSTS and a POST belong to an article.
If this is true then you have to do following.
1: In USER model define a relation to get all posts. like below.
public function posts() {
// Foreign key will be a key that is stored in posts table and represent the user MAY BE: user_id
$this->hasMany(Posts::class, 'foreign_key', 'local_key')
}
This will allow you to get all posts belong to a user.
2: In posts, model defines a user relation like below.
public function user() {
$this->belongsTo(User::class, 'foreign_key', 'local_key');
}
This will allow you to get a post User;
3: Now in your controller you will have something like this.
public function show($user_id) {
// find a user with posts as eager loading(to avoid query again)
$user = User::with(['posts'])->where('id', $user_id)->first();
// get all posts that belong to this user
$posts = $user->posts;
}
In controller show($user_id) method you will have a user data as well as user posts data. Now if you want to get a post relations then simply define as below. let say a post belongs to an article as well.
4: In posts, model defines a relation to get an article.
public function article() {
// This will allow you to get a post artcle
$this->belongsTo(Article::class, 'foreign_key', 'local_key');
}
Now you can get the article as well while finding a user. please see below. I am rewriting controller show action to give you a better understanding.
5: Get a user with user_id
public function show($user_id) {
// find a user with posts as eager loading(to avoid query again)
// eager loading for posts & post child, this will give you NOSQL at runtime and all data will come from one query.
$user = User::with(['posts', 'posts.article'])->where('id', $user_id)->first();
// get all posts that belong to this user
$posts = $user->posts;
foreach($posts as $post) {
$article = $post->article; // Child relation of post.
}
}
Hope you will understand the flow, you have to make sure models relation to work it perfectly. If you need further help please let me know.

Categories