Showing name in url instead of id? - php

I am building a small app in Laravel, but I needed to use some help. I read similar tasks but I don't see them related to what I have.
The app that I'm building is a dictionary and I really don't get the part where words are displayed by id in the URL. I tried to revert the logic for the curds (store, update, delete, show) so that they perform actions using the word name and not the word id. That worked. The redirecting to route app/{name} after the actions does work too.
Now, I presume this is not how it needs to be done, when the curds aren't taking actions on the id?
I also read about slugs, but since I'm building a dictionary, don't understand why I need to separate the word by a "-"? It's a single word, not like posts. And why do I need to have an extra field in my form for the slug, as the slug must be equal to the name? I'm getting here an error: the slug->unique() field must be not null.
If somebody has the time, please explain to me what's the best way for making the names of the words being displayed in the URL. Thank you, I appreciate it.
Edit
Here is my code for the update crud:
public function update(UpdateWordRequest $request, $name)
{
Word::query()->where('name', $name)->firstOrFail()->update($request->input());
return redirect()->route('dictionary.show', ['name' => $name])->with('success', 'Edited successfully!');
}
It's working fine, but when I re-edit the word and change it to word2, with a number, I then get this error: Trying to get property 'name' of non-object.
What do I need to make of it? Thank you :)!

I think you should be able to use a trait to fix this issue.
I would create a trait call hasSlug and have the following code:
<?php
namespace App\Traits;
trait HasSlug
{
protected static function bootHasSlug()
{
static::creating(function ($model) {
$columnName = static::getSlugColumn();
$model->$columnName = str_slug($model->name); //convert your name to slug
});
}
public function getSlugAttribute()
{
$columnName = static::getSlugColumn();
return (string) $this->attributes[$columnName];
}
protected static function getSlugColumn()
{
if (isset(static::$slugColumn)) {
return static::$slugColumn;
}
return 'slug';
}
}
So now if you use HasSlug trait to your model every time you create a record it will go and look for name field from your model and create a slug out of it.
Be sure to apply unique constrain to your name field if needed.
For more robust have a look at spatie slug package https://github.com/spatie/laravel-sluggable

Here is what I finally come up with.
Word.php
I used as #usrNotFound suggested spatie/laravel-sluggable.
<?php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Word extends Model
{
use HasSlug;
/**
* #return SlugOptions
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->slugsShouldBeNoLongerThan(20);
}
/**
* #param $query
* #param $slug
* #return mixed
*/
public function scopeFindBySlug($query, $slug)
{
return $query->where('slug', $slug)
->get();
}
}
WordController.php
<?php
namespace App\Http\Controllers;
use App\Word;
use App\Http\Requests\CreateWordRequest;
use App\Http\Requests\UpdateWordRequest;
class WordController extends Controller
{
/**
* Store a newly created resource in storage.
* #param CreateWordRequest $request
* #return mixed
*/
public function store(CreateWordRequest $request)
{
$word = Word::query()
->create($request->input());
return redirect()
->route('dictionary.show', $word->slug)
->with('success', 'Created successfully!');
}
/**
* Display the specified resource.
* #param $slug
* #return mixed
*/
public function show($slug)
{
$word = Word::whereSlug($slug)
->firstOrFail();
return view('word.show')
->with('word', $word);
}
/**
* Update the specified resource in storage.
* #param UpdateWordRequest $request
* #param $slug
* #return mixed
*/
public function update(UpdateWordRequest $request, $slug)
{
Word::whereSlug($slug)
->firstOrFail()
->update($request->input());
return redirect()
->route('dictionary.show', $slug)
->with('success', 'Edited successfylly!');
}
/**
* Remove the specified resource from storage.
* #param $slug
* #return mixed
*/
public function destroy($slug)
{
Word::whereSlug($slug)
->delete();
return redirect()
->route('dictionary.index')
->with('success', 'Deleted successfully');
}
}
Route.php
Route::get('/dictionary/{slug}', 'WordController#show')
->name('dictionary.show')
->where('slug', '[-A-Za-z0-9_-]+');
Creating, updating, destroying new words works fine. Eventually there is a small problem. When I want to update a particular word and change the name of it, after saving the word, I'm getting
Sorry, the page you are looking for could not be found.
and if I look at the url I see the old name instead of the new one. Has this to do with my update method and how do I fix it?

Related

Sharing Data with Multiple Views - Refactoring

I am looking for a better way to code this. I have been pounding my head into my desk because I just cant seem to figure it out.
I have an application that has multiple companies in it, and each company has their own resources. For example routes would look like this:
test.app/organizations/2/contacts
I have code in the views that rely on the {organization} variable being there. So on every single function I am having to add organization and return it with the view.
I would like to simplify it with like a View::Share or something but the organization doesn't apply to every single view.
This is how it looks now. Is there a better way of doing this? Any guidance would greatly be appreciated!
/**
* Display a listing of the resource.
*
* #return Application|Factory|Response|View
*/
public function index(Organization $organization)
{
return view('app.pages.organization.contact.index', [
'organization' => $organization
]);
}
/**
* Return data for datatables.
*
*/
public function datatable(Organization $organization)
{
$query = $organization->contacts();
return DataTables::of($query)->toJson();
}
Okay so this is what I have figured out after listening to headspace :).
I created a ViewComposer and returned the variable in the specific view composer like this:
protected $organization;
public function __construct(Organization $organization)
{
// Dependencies are automatically resolved by the service container...
$this->organization = $organization;
}
/**
* Bind data to the view.
*
* #param \Illuminate\View\View $view
* #return void
*/
public function compose(View $view)
{
$view->with('organization', $this->organization);
}
And then in my ViewServiceProvider I specified the pages using a wildcard of what I would like it to apply to. I am surprised I actually got this working. I just hope its the fastest/least resource intensive way of doing it.
public function boot()
{
View::composer('app.pages.organization.*', OrganizationComposer::class);
}

relatableQuery() for two resource fields on same model in Laravel Nova

I have a relationship between work 'days' and projects of different types. So my 'days' record has a reference to my 'projects' table twice because I have two different types of projects called 'Series' and 'Event'.
In my 'days' resource I've created two fields as such:
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Project::class)->sortable()->nullable(),
What I'm trying to do is filter the projects by their types so I've created this:
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
I've tried making relatableSeries and relatableEvents but they don't work. How can I make this connect to the fields correctly without having to create two separate tables for 'series' and 'events'.
The relatableQuery above winds up filtering both resource fields.
Because relatableQuery() is referencing a relatableModel() (so relatableProjects() references the Project model) I was able to create another model solely for the purpose of helping with this.
I created an Event model which references the same projects table and then was able to create a relatableEvents() method to use the where() filter query.
Note: I did have to also create an Event resource which references the Event model since this is how Nova works but was able to hide it from being accessed which you can find more information about here
See revised BelongsTo fields and new model below:
Day resource
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableEvents(NovaRequest $request, $query){
return $query->where('type', 'Event');
}
/**
* Get the fields displayed by the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function fields(Request $request)
{
return [
ID::make()->hideFromIndex()->hideFromDetail()->hideWhenUpdating(),
BelongsTo::make('User','user',User::class)->sortable(),
BelongsTo::make('Budget','budget',Budget::class)->sortable()->nullable(),
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Event::class)->sortable()->nullable(),
DateTime::make('Last Updated','updated_at')->hideFromIndex()->readOnly(),
new Panel('Schedule',$this->schedule()),
new Panel('Time Entry',$this->timeEntries()),
];
}
Event model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'projects';
protected $casts = [
'starts_on' => 'date',
'ends_on' => 'date',
];
public function event(){
return $this->belongsTo('App\Event');
}
public function project(){
return $this->belongsTo('App\Project');
}
}
i know it is an old question but i was facing same problem with laravel nova belongsto field, in some resource i have a belongsto that relates to users but these should be of 'supervisor' role in another resource i hace a belongsto field relating to users but these should be of role 'guard', as laravel nova belongsto field just takes all users in both selects all users appeared and it seems nova belongsto field doe not have a way, or at least i did not find it to scope the query, so what i did was creating a php class named BelongstoScoped this class extends laravel nova field BelongsTo so i overwrote the method responsible of creating the query
<?php
namespace App\Nova\Customized;
use Laravel\Nova\Query\Builder;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Http\Requests\NovaRequest;
class BelongsToScoped extends BelongsTo
{
private $modelScopes = [];
//original function in laravel belongsto field
public function buildAssociatableQuery(NovaRequest $request, $withTrashed = false)
{
$model = forward_static_call(
[$resourceClass = $this->resourceClass, 'newModel']
);
$query = new Builder($resourceClass);
//here i chaned this:
/*
$query->search(
$request, $model->newQuery(), $request->search,
[], [], ''
);
*/
//To this:
/*
$query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
*/
//The method search receives a query builder as second parameter, i just passed the result of custom function
//addScopesToQuery as second parameter, thi method returns the same query but with the model scopes passed
$request->first === 'true'
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
: $query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
return $query->tap(function ($query) use ($request, $model) {
forward_static_call($this->associatableQueryCallable($request, $model), $request, $query, $this);
});
}
//this method reads the property $modelScopes and adds them to the query
private function addScopesToQuery($query){
foreach($this->modelScopes as $scope){
$query->$scope();
}
return $query;
}
// this method should be chained tho the field
//example: BelongsToScoped::make('Supervisores', 'supervisor', 'App\Nova\Users')->scopes(['supervisor', 'active'])
public function scopes(Array $modelScopes){
$this->modelScopes = $modelScopes;
return $this;
}
}
?>
In my users model i have the scopes for supervisors and guard roles like this:
public function scopeActive($query)
{
return $query->where('state', 1);
}
public function scopeSupervisor($query)
{
return $query->role('supervisor');
}
public function scopeSuperadmin($query)
{
return $query->role('superadmin');
}
public function scopeGuarda($query)
{
return $query->role('guarda');
}
So in the laravel nova resource i just included the use of this class
*remember the namespace depends on how you name your file, in my case i created the folder Customized and included the file there:
use App\Nova\Customized\BelongsToScoped;
In the fields in nova resource i used like this:
BelongsToScoped::make('Supervisor', 'supervisorUser', 'App\Nova\Users\User')
->scopes(['supervisor', 'active'])
->searchable()
So that way i could call the belongsto field in the nova resources which filter users depending on modle scopes.
I hope this helps someone, sorry if my English is not that good.

Laravel Get Slug from Database

I'm attempting to build a blog like application with the latest version of laravel. I'm trying to figure out how to pull a slug from the database for each article and then route it to all work correctly.
I've got it kind of working but the content wont display on the article if you use the slug to view it.
localhost/articles/1 - works fine, content shows on the page (title etc)
localhost/articles/installing-example - this works but the content errors
This happens when you try to navigate to the page using the slug from the database: Trying to get property 'title' of non-object (View: C:\xampp\htdocs\blogtest\resources\views\articles\show.blade.php)
Error with this line: <h1><?php echo e($articles->title); ?></h1>
app/http/controllers/ArticlesController:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Article;
class ArticlesController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$articles = Article::all();
return view('articles.index')->with('articles', $articles);
}
/**
* Show the form for creating a new resource.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$articles = Article::find($id);
return view('articles.show')->with('articles', $articles);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param int $id
* #return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
app/Article.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $table = 'articles';
public $primaryKey = 'id';
public $timestamps = true;
}
routes/web.php
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Route::resource('articles', 'ArticlesController');
resources\views\articles\show.blade.php
#extends('layouts.master')
#section('content')
<h1>{{$articles->title}}</h1>
#endsection
database
Any help and suggestions will be appreciated thanks.
Trying to get property 'title' of non-object (View: C:\xampp\htdocs\blogtest\resources\views\articles\show.blade.php)
Implies that that the $articles is not an object, if you dump it, it should be outputting null - quite rightly so.
The find function is to be used to find a row using your primary key and your primary key is not your slug column, therefore it cannot find a record.
You need to setup a route to accept a {slug} and then based on the slug you need to make the following change:
$articles = Article::find($id);
To,
$articles = Article::where('slug', $slug)->first();
and ensure that $slug is the actual slug and not the id column value.
You need to set route key in your model
// Article model
public function getRouteKeyName(){
return 'slug';
}
Try this
Without slug
Route
Route::resource('articles', 'ArticlesController');
/article/{id}
Controller
public function show($id)
{
$articles = Article::first($id);
return view('articles.show', compact('articles'));
}
Blade
#extends('layouts.master')
#section('content')
#if(!is_null($articles))
<h1>{{$articles->title}}</h1>
#endif
#endsection
With Slug
Route
Route::get('/articles/{slug}', ['as'=>'articles.by.slug', 'uses'=> 'ArticlesController#showArticles']);
or
Route::get('/articles/{slug}', 'ArticlesController#showArticles')->name('articles.by.slug');
/article/{slug}
Controller
public function showArticles($slug)
{
$articles = Article::where('slug', $slug)->first();
return view('articles.show', compact('articles'));
}
Blade
#extends('layouts.master')
#section('content')
#if(!is_null($articles)) //or #if($articles)
<h1>{{$articles->title}}</h1>
#endif
#endsection
This Is because you getting null data from database.
Reason is, you passing slug in the id field so
localhost/articles/installing-example
you passing id = installing-example, hence no id is found so no data you get.
so for this
convert your slud into original form and search not with id, search with your slug field.
let suppose from the title you make slug.
title is installing example
so when you get slug, make it into original form
then search with original like
$articles = Article::where('titile',$original)->first();

Laravel: Check with Observer if Column was Changed on Update

I am using an Observer to watch if a user was updated.
Whenever a user is updated I would like to check if his email has been changed.
Is something like this possible?
class UserObserver
{
/**
* Listen to the User created event.
*
* #param \App\User $user
* #return void
*/
public function updating(User $user)
{
// if($user->hasChangedEmailInThisUpdate()) ?
}
}
Edit: Credits to https://stackoverflow.com/a/54307753/2311074 for getOriginal
As tadman already said in the comments, the method isDirty does the trick:
class UserObserver
{
/**
* Listen to the User updating event.
*
* #param \App\User $user
* #return void
*/
public function updating(User $user)
{
if($user->isDirty('email')){
// email has changed
$new_email = $user->email;
$old_email = $user->getOriginal('email');
}
}
}
If you want to know the difference between isDirty and wasChanged, see https://stackoverflow.com/a/49350664/2311074
You don't have to get the user again from the database. The following should work:
public function updating(User $user)
{
if($user->isDirty('email')){
// email has changed
$new_email = $user->email;
$old_email = $user->getOriginal('email');
}
}
A little late to the party, but might be helpful for future devs seeking a similar solution.
I recommend using the package Laravel Attribute Observer, as an alternative to polluting your Service Providers or filling your Observers with isDirty() and wasChanged() boilerplate.
So your use case would look like this:
class UserObserver
{
/**
* Handle changes to the "email" field of User on "updating" events.
*
* #param \App\User $user
* #param string $newValue The current value of the field
* #param string $oldValue The previous value of the field
* #return void
*/
public function onEmailUpdating(User $user, string $newValue, string $oldValue)
{
// Your logic goes here...
}
}
Laravel Attribute Observer is especially useful when you have a lot of attributes to observe on one or more models.
Disclaimer: I am the author of Laravel Attribute Observer. If it saves you some development time, consider buying me a coffee.
Instead of using the updating event, You should use the updated model event to compare the column changes against the original. Please have a look at the below code.
AppServiceProvider.php
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
User::updated(function($user){
$changes = array_diff($user->getOriginal(), $user->getAttributes());
if(array_key_exists('email',$changes)){
/* do your things. */
}
});
}
}
Explanation:
Once you get the difference then you need to use array_key_exists to check whether the email field got updated or not.

Imitating Drupal url aliases in Laravel 5.2

In Drupal there is a simple url rewrite system that stores path aliases and the real route in the database.
For example:
/category/hello => node/5
I would like to imitate this system in Laravel.
I know how to create the database structure. What I would like suggestions for is actually overriding and remapping the incoming request.
I've taken a glance at the router. No events are really sticking out. What I would like to avoid is adding every permutation as a static route. I would like to for this to be completely dynamic.
I was reading middleware with a redirect would work but don't know if that is the best route to go. Keep in mind that the aliases could be anything. There isn't any set pattern.
The actual business case for this is the application has a hierarchy of categories like for a catalog on an ecommerce site. For every path a dynamic page will need to exist and possibly also allow pass-thrus to other pages.
Ex.
/sports/football/nfl => \App\Http\Controllers\Category::lp(2)
Even something like:
/sports/football/nfl/:game/lines => \App\Http\Controllers\Lines::lp(:game)
However, I don't want to have every permutation in the db. Just the base one and allow everything after /sports/football/nfl/* pass thru to a completely different location.
If I do recall in Symfony this could be done with a custom route matcher. However, I don't see anything like that in Laravel. Unless I'm just missing something. It looks like you either add a static route or nothing all but I haven't taken the deep dive into that code yet so could be wrong.
I was able to implement a dynamic routing system by creating my own custom route and adding to the route collection manually.
Custom Route
use Illuminate\Routing\Route as BaseRoute;
use Modules\Catalog\Routing\Matching\CategoryValidator;
use Illuminate\Routing\Matching\MethodValidator;
use Illuminate\Routing\Matching\SchemeValidator;
use Illuminate\Routing\Matching\HostValidator;
use Illuminate\Http\Request;
use Modules\Event\Repositories\CategoryRepository;
use Illuminate\Routing\ControllerDispatcher;
/**
* Special dynamic touting for catalog categories.
*/
class CategoryRoute extends BaseRoute {
protected $validatorOverrides;
/**
* #param CategoryRepository
*/
protected $categoryRepository;
/**
* Create a new Route instance.
*
* #param CategoryRepository $categoryRepository
* The category repository.
*/
public function __construct(CategoryRepository $categoryRepository)
{
$this->categoryRepository = $categoryRepository;
$action = [
'uses'=> function() use ($categoryRepository) {
$path = app('request')->path();
$category = $categoryRepository->findOneByHierarchicalPath($path);
$controller = app()->make('Modules\Catalog\Http\Controllers\Frontend\CategoryController');
return $controller->callAction('getIndex', ['categoryId'=>$category->getId()]);
}
];
$action['uses']->bindTo($this);
parent::__construct(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'],'_catalog_category',$action);
}
/**
* Determine if the route matches given request.
*
* #param \Illuminate\Http\Request $request
* #param bool $includingMethod
* #return bool
*/
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute();
$validators = $this->getValidatorOverrides();
foreach ($validators as $validator) {
/*if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}*/
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
/**
* Get the route validators for the instance.
*
* #return array
*/
public function getValidatorOverrides()
{
if (isset($this->validatorOverrides)) {
return $this->validatorOverrides;
}
$this->validatorOverrides = [
new MethodValidator, new SchemeValidator,
new HostValidator, /*new UriValidator,*/
new CategoryValidator($this->categoryRepository)
];
return $this->validatorOverrides;
}
}
Custom Route Validator
<?php
namespace Modules\Catalog\Routing\Matching;
use Illuminate\Routing\Matching\ValidatorInterface;
use Illuminate\Routing\Route;
use Illuminate\Http\Request;
use Modules\Event\Repositories\CategoryRepository;
class CategoryValidator implements ValidatorInterface
{
protected $categoryRepository;
public function __construct(CategoryRepository $categoryRepository) {
$this->categoryRepository = $categoryRepository;
}
/**
* Validate a given rule against a route and request.
*
* #param \Illuminate\Routing\Route $route
* #param \Illuminate\Http\Request $request
* #return bool
*/
public function matches(Route $route, Request $request)
{
$path = $request->path() == '/' ? '/' : '/'.$request->path();
$category = $this->categoryRepository->findOneByHierarchicalPath($path);
return $category?true:false;
}
}
To satisfy the requirements of the category repository dependency I had to also create a subscriber that adds the route after all the providers had been booted. Simply placing it in the routes.php file would not work because there was no guarantee that all the dependencies for IoC would be configured when that file gets loaded.
Bootstrap Subscriber
use Modules\Catalog\Routing\CategoryRoute;
use Modules\Event\Repositories\CategoryRepository;
use Illuminate\Support\Facades\Route as RouteFacade;
class BootstrapSubscriber {
public function subscribe($events) {
$events->listen(
'bootstrapped: Illuminate\Foundation\Bootstrap\BootProviders',
'Modules\Catalog\Subscribers\BootstrapSubscriber#onBootstrappedBootProviders'
);
}
public function onBootstrappedBootProviders($event) {
$categoryRepository = app(CategoryRepository::class);
RouteFacade::getRoutes()->add(new CategoryRoute($categoryRepository));
}
}
I will probably expand on this but that is the basic way to do it.

Categories