Laravel - how to bind soft deleted route and model - laravel

I'm using soft delete in model Article, but in model Comment not use soft delete. I'm also customize the key using slug column in model Article. If the article is deleted, I want still show the comment. But when article is deleted, show method always return 404.
public function show(Article $article, Comment $comment)
{
if ($article->id != $comment->article_id)
throw new NotFoundHttpException('Record Not Found.');
return $this->success(['comment => $comment']);
}
How to fix this?

Your question statement is not defining the problem you should ask how to bind soft deleted route and model.
Laravel provide ->withTrashed() method for this so it also bind soft deleted models in route.
web.php
user App/Http/Controller/ArticleController;
Route::get('article/{article}', [ArticleController::class, 'show'])->name('article.show')->withTrashed();
But this method added in Laravel 8.55 If you have older version so you can simply find model in controller without route model binding.
ArticleController.php
public function show($article, App/Comment $comment)
{
$article = App/Article::withTrashed()->findOrFail($article);
if ($article->id != $comment->article_id) {
throw new NotFoundHttpException('Record Not Found.');
}
return $this->success(['comment => $comment']);
}
Or you can also use Explicit Binding for specific model in RouteServiceProvider.
public function boot()
{
parent::boot();
Route::bind('article', function ($id) {
return App\Article::withTrashed()->find($id) ?? abort(404);
});
}
And you can also use onlyTrashed() method in explicit binding in case you use separate route for trashed models.

If you want to get deleted records as well, use the method withTrashed
Your code should look something like this:
Article::withTrashed()->find($id);
Hope it help u and happy coding !

Related

Laravel, eloquent and foreach in controller

I'm new in Laravel and I'm curious about one thing. I have 3 database tables: posts, comments, replies. I want to make a simple delete from each. But obviously post has many comments and comments has many replies. Whole thing is about these replies. Seems like I can't reach them.
I have properly working relations between tables.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Post;
use App\Comment;
use App\Reply;
use App\Traffic;
use Image;
use Illuminate\Support\Facades\Storage;
class PostsController extends Controller
{
//few others things here...
public function destroy($id) //$id is an id of post
{
// Select Post and comments of post
$post = Post::find($id);
$comments = Comment::where('post_id', $id);
//remove image (working fine)
Storage::delete('public/img/' . $post->image);
// delete all replies of post comments <<< Here is the issue. Can I even do like this?
foreach ($comments as $comment) {
$post_comment_reply = Reply::where('comment_id', $comment->id);
$post_comment_reply->delete();
}
// delete post record (working fine)
$post->delete();
//delete comments of post (working fine)
$comments->delete();
// return to user profile (working fine)
return redirect('/home')->with('success', 'Post has been deleted');
}
There is an even easier way to do so.. if you just add a database constraint to the foreign key in the replies table to the comment..
$table->unsignedInteger('comment_id');
$table->foreign('comment_id')->references('id')->on('comments')
->onDelete('cascade');
The last part: onDelete('cascade') ensures that all the replies will be deleted once a comment has been deleted :) so you don't have to manually do that in the application layer.
Let me know if it makes sense :)
Instead of deleting the replies in a loop, you can delete them all at once:
$comments = Comment::where('post_id', $id);
$comment_ids = $comments->pluck('id');
Reply::whereIn('comment_id', $comment_ids)->delete();
What's wrong in your code is that you create a db query but does not execute it:
// You forgot the ->get() following the where statement
foreach ($comments as $comment)
$post_comment_reply = Reply::where('comment_id', $comment->id)->get();
$post_comment_reply->delete();
}
However the code altogether is not quite optimal, you could make it directly on database level with onDelete('cascade'), or simply create a request to delete the replies without retrieving them and reducing the number of query to the db, like such:
foreach ($comments as $comment)
Reply::where('comment_id', $comment->id)->delete();
}
One step further reducing db queries like suggested above:
Reply::whereIn('comment_id', $comments->pluck('id'))->delete();
If you want to delete the relations via Laravel, you have to override the boot function.
Override the boot() on your Comment model like
protected static function boot()
{
static::deleting(function (Comment $model) {
$model->replies()->delete();
});
parent::boot();
}
This will delete all the replies associated to a comment when that comment is deleted via eloquent.

Laravel Route with Multiple Parameters - How to get correct database result

I am a couple of weeks in to learning Laravel and have come across a problem which I can not find the answer to by myself, or online.
I am building a directory website with urls structured like:
directory.co.uk/parks
directory.co.uk/parks/{county-name}
directory.co.uk/parks/{county-name}/{park-name}
As {park-name} is not unique, I am struggling to return the page for an individual park. The controller needs to look up the county.id using the county.slug and then the park.id using the park.county_id and the park.slug.
I have routes in the web.php file such as:
Route::get('/parks','ParksController#index')->name('parks');
Route::get('/parks/{county}/{park}','ParksController#show')->name('park');
I have Parks and Counties models and (belongsTo and hasMany relationships set up between the two).
I have this is in both models:
public function getRouteKeyName()
{
return 'seo_url';
}
Then in my ParksController, I am at a loss. I currently have:
public function show(Counties $county, Parks $park)
{
//return $park;
//dd($park);
return view('parks.park', ['park'=>$park]);
}
I have also tried the non-Eloquent way:
public function show($county_slug,$park_slug)
{
$county = DB::table('counties')->where('seo_url',$county_slug)->get();
$county_id = $county->pluck('id');
$park = DB::table('parks')->where('county_id', $county_id)->where('seo_url', $park_slug)->get();
//dd($county_id);
//return $park;
return view('parks.park', ['park'=>$park]);
}
This returns a 404 error. Any help would be much appreciated. (I have done a lot of reading on Route model binding, but can not see any examples like mine.)
Laravel has an undocumented feature in its explicit model binding, where the callback can be given the current Route the binding is for. This can allow you to access the other parameters and use them to add conditionals.
Router::bind('park', static function ($value, Route $route) {
$query = Parks::where('seo_url', '=', $value);
if ($route->hasParameter('county')) {
$county = $route->parameter('county');
$query->where('county_id', '=', $county instanceof Counties ? $county->id : $county);
}
return $query->first() ?? abort(404);
});

Route Parameters laravel issue

I simply made this but /maps/{category}/{map} does not work correctly.
putting anything on {category} showing same result.
/maps/php/1
/maps/laravel/1
I want to show a result when category's name and map's id exactly matches otherwise redirect to homepage.
My route
Route::get('/maps/{category}', 'MapsController#index');
Route::get('/maps/{category}/{map}', 'MapsController#show');
My controller
public function show(Category $category, Map $map)
{
return view('maps.show', compact('map'));
}
My blade template
{{ $map->title }}
Swap over the routes in your routes file so that the more specific one is first.
Route::get('/maps/{category}/{map}', 'MapsController#show');
Route::get('/maps/{category}', 'MapsController#index');
Firstly, there is no need to swap order of routes if you are using 5.4, since I started with 5.4 to use laravel i cannot say anything for previous versions.
If you you want to filter asked map if it matches given category, you can use "whereHas" method with eloquent if you defined relationship between category and map.
"One To Many (Inverse)" relationship is what you need to use, check here: https://laravel.com/docs/5.4/eloquent-relationships#one-to-many-inverse
And querying relationship is what you need to know, check here: https://laravel.com/docs/5.4/eloquent-relationships#querying-relationship-existence
How your relationship should look like in Map model:
/**
* Get the category that owns the map.
*/
public function category()
{
return $this->belongsTo('App\Category');
}
An example code:
public function show($category, $map)
{
$map = Map::whereHas('category',function($query) use($category){
$query->where('id', $category);
});
return view('maps.show', compact('map'));
}
Route::get('/maps/{category}', 'MapsController#index');
is a more generalized route so it matches more than
Route::get('/maps/{category}/{map}', 'MapsController#show');
You should list the show route first.
To match an exact model attribute (i.e. name), you should customize the resolution logic the router uses in the route service provider boot method. For example:
Route::bind('category', function ($value) {
return App\Category::where('name', $value)->first();
});
Route::bind('map', function ($value) {
return App\Map::where('id', $value)->first();
});
public function show($category,$map)
{
$category = App\Category::where('category_name',$category);
$map_id =App\Map::where('map_id',$map);
if(!empty($category) && !empty($map_id))
{
return view('maps.show', compact('map'));
}
else
{
return view('homepage', compact('map'));
}
}

Nested Eager loading relationships in Laravel

I am creating a forum software using Laravel and I'm trying to access the most recent posts made within a specific topic using latestPost as defined in my model below
public function latestPost() {
return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
}
Here is how the nested relationships are defined in App\Post.php
public function author()
{
return $this->belongsTo('App\User', 'author_id');
}
public function thread()
{
return $this->belongsTo('App\Thread', 'thread_id');
}
public function topic() {
return $this->belongsTo('App\Topic');
}
I have been able to access posts of threads, authors etc no problem, and when I have {{ $topic->latestPost }} in my views, it will list the object including relationships successfully. But as soon as I try to display a specific part such as {{ $topic->latestPost->author->username }} I am getting the following error: Trying to get property of non-object
In this specific view, topics is yet another relationship pulled from Categories like so in the Controller and getting $topics using a #foreach on $categories->topics:
public function index() {
$categories = Category::orderBy('order', 'asc')->get();
return view('welcome', compact('categories'));
}
Here is a dump of $categories: http://dumptext.com/H8Nq16ea
Maybe you want to use nested eager loading.
Change: return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
To this: return $this->hasOne('App\Post')->with('thread.author')->latest();
Also i never heard about latest() and the end of your return statement. Maybe you can try it with orderBy as shown below.
`return $this->hasOne('App\Post')->with('thread.author')->orderBy('id')->first;`
I guess you can use the orderBy('id')->reverse() function if the result is not in the order you want. Let me know if it helped you out.
You should make sure you have author for latest post you want to display. Otherwise if you expect not to have one for each record, you should try:
{{ $topic->latestPost->author ? $topic->latestPost->author->username : 'No author' }}
In addition instead of:
return $this->hasOne('App\Post')->with('thread')->with('author')->latest();
you can use
return $this->hasOne('App\Post')->with('thread','author')->latest();

Fluently updating an existing one to one relationship with Eloquent in Laravel 5

I have a one-to-one relationship between my User model and an additional UserInformation model in which I store additional needed information which would bloat the "normal" user table.
I set up my models like this:
# User.php
public function information()
{
return $this->hasOne(UserInformation::class);
}
# UserInformation.php
public function user()
{
$this->belongsTo(User::class);
}
I have a profile page where the User can update information from both tables.
The view has inputs like this:
<input name="email"> // is a field in the users-table
<input name="information[size]"> // is a field in the users-information table
I read in different locations that I should be able to save both my User model and its relation in with:
$user->fill($request->all())->save();
But this throws the following error:
preg_replace(): Parameter mismatch, pattern is a string while replacement is an array
So my current solution looks like this:
auth()->user()
->fill($request->except('information'))
->save();
auth()->user()
->information
->fill($request->input('information'))
->save();
That works very good but doesn't look good in my opinion. So my question is: how can I clean that code up and save both in one go?
Have you tried including this
protected $guarded = array('information');
in your User.php model file
and then
auth()->user()
->fill($request->all())
->information->fill($request->input('information'))
->save();
I think your current solution looks fine, but if you want, you could always extract it out to your own custom method in your User model.
public function saveWithInformation($attributes)
{
$this->fill($attributes)->save();
$this->information->fill($attributes['information'])->save();
}
Then you can just call:
auth()->user()->saveWithInformation($request->all());
Since I was looking for a pretty flexible solution I came up with this function which I implemented into my User model (but it could also be included in a BaseModel)
public function fillWithRelation(array $request)
{
foreach ($request as $key => $value)
{
if (is_array($value) && method_exists($this, $key))
// check if the value is an array and if a method with the name of
// the key exists (which would be the relationship
{
$this->{$key}->fill($value);
unset($request[$key]);
}
}
return $this->fill($request);
}
This is definitely working if you include the information for a hasOne relationship like posted in my question.

Resources