Extend Resource Controllers - laravel

I'm doing what I want in a certain way and I'm looking for alternatives or better ways of doing.
I'm using Resource Controllers in my application. Also I'm using softdelete in several models, so my routes are as follows:
Route::get('users/deleted', array('uses' => 'UserController#trash'));
Route::put('users/{id}/restore', array('uses' => 'UserController#restore'));
Route::resource('users', 'UserController');
The first route is to display objects that have been deleted.
The second allows me to restore these deleted elements.
The third maps the traditional methods (create, edit, update, etc.).
I have several controllers that work exactly the same way, I wonder if have any way to tell laravel to work with this two methods (trash and delete) by default without the two extra lines.
Is it possible? Or to give a better way that I'm doing?
(sorry for bad english)

Keeping things simple and DRY.
You can extend the Router and replace the Route Facade in your app/config/app.php file but seems like a lot of work for not much gain, but don't forget that your routes file is a PHP script and you can do things like:
$routes = [
['users' => 'UserController'],
['posts' => 'PostController'],
];
foreach ($routes as $key => $controller)
{
Route::get("$key/deleted", array('uses' => "$controller#trash"));
Route::put("$key/{id}/restore", array('uses' => "$controller#restore"));
Route::resource($key, $controller);
}
Extending the router
To extend the router you need to create 3 classes:
The Router extended, where you'll add your new methods:
<?php namespace App\Routing;
class ExtendedRouter extends \Illuminate\Routing\Router {
protected $resourceDefaults = array(
'index',
'create',
'store',
'show',
'edit',
'update',
'destroy',
'deleted',
'restore',
);
protected function addResourceDeleted($name, $base, $controller)
{
$uri = $this->getResourceUri($name).'/deleted';
return $this->get($uri, $this->getResourceAction($name, $controller, 'deleted'));
}
protected function addResourceRestore($name, $base, $controller)
{
$uri = $this->getResourceUri($name).'/{resource}/restore';
return $this->get($uri, $this->getResourceAction($name, $controller, 'restore'));
}
}
A Service Provider, to boot your new router, using the same IoC identifier Laravel uses ('router'):
<?php namespace App\Routing;
use Illuminate\Support\ServiceProvider;
class ExtendedRouterServiceProvider extends ServiceProvider {
protected $defer = true;
public function register()
{
$this->app['router'] = $this->app->share(function() { return new ExtendedRouter($this->app); });
}
public function provides()
{
return array('router');
}
}
And a Facade, to replace Laravel's one
<?php namespace App\Facades;
use Illuminate\Support\Facades\Facade as IlluminateFacade;
class ExtendedRouteFacade extends IlluminateFacade {
public static function is($name)
{
return static::$app['router']->currentRouteNamed($name);
}
public static function uses($action)
{
return static::$app['router']->currentRouteUses($action);
}
protected static function getFacadeAccessor() { return 'router'; }
}
Then you need to add your Service Provider and Facade to your app/config/app.php file, commenting the Laravel original ones.

Im using route model bind.
I do this way.
<?php
namespace App\Http\Router;
use Illuminate\Support\Facades\Route;
use Illuminate\Routing\Router;
class CustomRouter extends Router
{
public function customResource($routeName, $controllerPath, $routeBindName = null, $routeBindModel = null) {
$routeBindName = $routeBindName ?? $routeName;
$routeBindNameTrashed = "{$routeBindName}_trashed";
if($routeBindModel && $routeBindName) {
Route::model($routeBindName, $routeBindModel);
Route::bind($routeBindNameTrashed, function($modelId) use ($routeBindModel) {
return app($routeBindModel)->newQuery()->onlyTrashed()->findOrFail($modelId);
});
}
Route::put("$routeName/{{$routeBindNameTrashed}}/restore", "{$controllerPath}#restore")->name("{$routeName}.restore");
Route::delete("$routeName/{{$routeBindNameTrashed}}/force", "{$controllerPath}#forceDelete")->name("{$routeName}.force-delete");
Route::resource($routeName, $controllerPath);
}
}
Alter bootstrap/app.php
$app->singleton(
'router',
App\Http\Router\CustomRouter::class
);
Then usage:
Route::customResource('projects', 'ProjectsController', 'project', \App\Models\Project::class);
Route:list

Works like a charm
$routes = [
'posts' => 'PostController',
'posts.comments' => 'PostCommentController',
];
foreach ($routes as $key => $controller)
{
Route::get("$key/deleted", array('uses' => "$controller#trash"));
Route::put("$key/{id}/restore", array('uses' => "$controller#restore"));
Route::resource($key, $controller);
}

Related

Larave 6 - pivot table sync - fire created event only if the attached user is new

I have following code:
$clinic->users()->sync($sync);
Which will go to this class (sync is working):
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ClinicUser extends Pivot
{
protected $table = 'clinic_user';
static function boot()
{
parent::boot();
static::created(function($item) {
$user = \App\User::find($item->users_id);
$clinic = \App\Models\Clinic::find($item->clinics_id);
if($user->userData->notification_email == 1)
\Mail::to($user->email)->send(new \App\Mail\ClinicManagerAdded(
$user,
$clinic));
if($user->userData->notification_app == 1)
\App\Notification::create([
'title' => "message",
'body' => "message",
'user_id' => $user->id,
]);
});
}
}
Is it possible to fire created method only to the new users (does which weren't detached)?
What i was suggesting is not that robust, infact you need to do
$clinic->users()->detach($sync->pluck('id'));
$clinic->users()->sync($sync);
Every time, and you need to remember it (and so is not robust).
What i feel to suggest you to do is something like this:
Delete the notification in the Model
Create a Service for this operation, let's call it NotyfyUsersNewClinicService (maybe you will find a better name):
<?php
namespace App;
use ...;
class NotyfyUsersNewClinicService{
public __constructor(){}
public updateUsers(Clinic& $clinic, Collection& $newUsers){
$clinic->users->diff($newUsers)->each(function(User $users){
$user->userData->notification_email = true;
\Mail::to($user->email)->send(new \App\Mail\ClinicManagerAdded(
$user,
$clinic));
});
$clinic->users()->sync($sync);
}
}
then you will only need to use this:
(new NotyfyUsersNewClinicService())->updateUsers($clinic, $users);
Note: better if you move the email to a job and send it using queue:work
If someone has a similar problem, I have managed to resolve this by creating the static variable, and fill this variable in the deleted event, like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ClinicUser extends Pivot
{
protected $table = 'clinic_user';
static $ids = [];
static function boot()
{
parent::boot();
static::deleted(function($item){
self::$ids[] = $item->users_id;
});
static::created(function($item){
if(!\in_array($item->users_id, self::$ids)){
$user = \App\User::find($item->users_id);
$clinic = \App\Models\Clinic::find($item->clinics_id);
if($user->userData->notification_email == 1)
\Mail::to($user->email)->send(new \App\Mail\ClinicManagerAdded(
$user,
$clinic));
if($user->userData->notification_app == 1)
\App\Notification::create([
'title' => "new message",
'body' => "<p>body</p>",
'user_id' => $user->id,
]);
}
});
}
}

Reading data from in vue/cli from Laravel ResourceCollection

In Laravel 6 backend rest api app I use ResourceCollection and Resourcem defintion like :
<?php
namespace App\Http\Resources;
use App\Facades\MyFuncsClass;
use Illuminate\Http\Resources\Json\ResourceCollection;
class TaskCollection extends ResourceCollection
{
public function toArray($task)
{
return [
$this->collection->transform(function($task){
return [
'id' => $task->id,
'name' => $task->name,
'slug' => $task->slug,
...
'events' => !empty($task->events) ? $task->events : [],
'events_count' => !empty($task->events_count) ? $task->events_count : 0,
'created_at' => $task->created_at,
'updated_at' => $task->updated_at,
];
}),
];
}
public function with($task)
{
return [
'meta' => [
'version'=>MyFuncsClass::getAppVersion()
]
];
}
}
I found this decision at
Laravel 5.5 API resources for collections (standalone data)
and it works for me, but I dislike the way I got data on client part, so for listing of data defined in control :
return (new TaskCollection($tasks));
I have to write in vue/cli app :
axios.post(this.apiUrl + '/adminarea/tasks-filter', filters, this.credentialsConfig)
.then((response) => {
this.tasks = response.data.data[0]
this.tasks_total_count = response.data.meta.total
this.tasks_per_page = response.data.meta.per_page
and when I need to get 1 item I define in laravel's control :
return (new TaskCollection([$task])); // I have to wrap it as array
and I have to write in vue/cli app :
axios.get(this.apiUrl + '/adminarea/tasks/' + this.task_id, this.credentialsConfig)
.then((response) => {
// console.log('response::')
// console.log(response)
//
this.taskRow = response.data.data[0][0]
I dislike syntax like data.data[0] and data.data[0][0] and even that works for me
In my app/Providers/AppServiceProvider.php I have lines :
<?php
namespace App\Providers;
use Auth;
use Validator;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Resource::withoutWrapping(); // looks like that does not work for ResourceCollection!
<?php
namespace App\Providers;
use Auth;
use Validator;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Resource::withoutWrapping();
...
If there is a way to get rid of data.data[0] and data.data[0][0] in vue/cli part ?
MODIFIED 2:
I created new resource with few columns as app/Http/Resources/Skill.php :
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class Skill extends JsonResource
{
public static $wrap = 'skills';
public function toArray($request)
{
return [
'id' => $request->id,
'name' => $request->name,
'user_id' => $request->user_id,
'user_name' => $request->user_name,
'skill_id' => $request->skill_id,
'skill_name' => $request->skill_name,
'rating' => $request->rating,
'created_at' => $request->created_at,
];
}
}
and app/Http/Resources/SkillCollection.php :
<?php
namespace App\Http\Resources;
use App\Facades\MyFuncsClass;
use App\Http\Resources\Skill;
use Illuminate\Http\Resources\Json\ResourceCollection;
class SkillCollection extends ResourceCollection
{
public static $wrap = 'skills';
public function toArray($request)
{
return $this->collection->transform(function($request){
parent::toArray($request);
});
}
and resulting I have empty "skills":[null,null,null,null] results. What is wrong ?
Thanks!
The first thing you can do is to use destructuring assignment to get the response data from your Axios request:
axios.get(this.apiUrl + '/adminarea/tasks/' + this.task_id, this.credentialsConfig)
.then(({ data }) => {
this.taskRow = data.data[0][0]
So now response.data.data[0][0] is shortened to data.data[0][0].
The second thing you can do is, in the toArray function of your TaskCollection class, to return the transformed collection directly instead of wrapping it in an array:
public function toArray($task)
{
return $this->collection->transform(function($task){
return [
'id' => $task->i
...
];
});
}
Now you don't need to use [0] everywhere. So instead of data.data[0][0], you can access a single task with data.data[0].
To avoid having to use [0] for singular responses, you should return a JsonResource instead of a ResourceCollection. For this you would need to move your code from inside $this->collection->transform to a new JsonResource class. For example:
app/Http/Resources/TaskResource.php:
<?php
namespace App\Http\Resources;
use App\Facades\MyFuncsClass;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray($task)
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
...
];
}
public function with($task)
{
return [
'meta' => [
'version'=>MyFuncsClass::getAppVersion()
]
];
}
}
Then you can return a single resource with
return new TaskResource($task);
In Javascript you will be able to get a single task with data.data now:
axios.get(this.apiUrl + '/adminarea/tasks/' + this.task_id, this.credentialsConfig)
.then(({ data }) => {
this.taskRow = data.data;
You can refactor your TaskCollection class to implicitly use your new TaskResource class for each task:
<?php
namespace App\Http\Resources;
use App\Facades\MyFuncsClass;
use Illuminate\Http\Resources\Json\ResourceCollection;
class TaskCollection extends ResourceCollection
{
public function toArray($request)
{
return parent::toArray($request);
}
public function with($task)
{
return [
'meta' => [
'version'=>MyFuncsClass::getAppVersion()
]
];
}
}
Like I tried to explain in the comments, you won't be able to get away from having to wrap your results in a key (like 'data') while returning non-null values from the with function. If you don't believe me, temporarily remove your with function and you'll see that you'll be able to access your tasks simply with data (if you have disabled wrapping with Resource::withoutWrapping();).
However, one thing you can do is to customise the "wrap" key. For example, for a collection of tasks you might want it to be tasks and for a single task, just task. You can simply add a $wrap property to your TaskCollection and TaskResource classes:
class TaskCollection extends ResourceCollection
{
public static $wrap = 'tasks';
...
class TaskResource extends JsonResource
{
public static $wrap = 'task';
Now you will be able to access a list of tasks from Javascript with data.tasks and a singular task with data.task:
axios.post(this.apiUrl + '/adminarea/tasks-filter', filters, this.credentialsConfig)
.then(({ data }) => {
this.tasks = data.tasks;
axios.get(this.apiUrl + '/adminarea/tasks/' + this.task_id, this.credentialsConfig)
.then(({ data }) => {
this.taskRow = data.task;

How make Route::resource with page and filter options?

In Laravel 5.8 making backend rest api app with resource defined in routes/api.php, as
Route::group(['middleware' => 'jwt.auth', 'prefix' => 'adminarea', 'as' => 'adminarea.'], function ($router) {
...
Route::resource('skills', 'API\Admin\SkillController');
now on clients part for listing of skills I need to add current_page and filter_name.
Can it be done with Route::resource definition ?
Actually when we making the resource controller in laravel then it created the method index
create,
store,
show,
edit,
update,
destroy.
method and routes of these respectively but when you are going to define the new method and for access this method then you need to create new route for this.
otherwise it is not possible with the resource route in laravel.
public function abc(Request $request)
{
dd($request->all());
}
route for this
route::post('/abc','ABCController#abc')->name('abc');
you can check the route by this command
php artisan route:list or
php artisan r:l
hope you understand.
After some search I found a decision in defining in routes one more post route:
Route::group(['middleware' => 'jwt.auth', 'prefix' => 'adminarea', 'as' => 'adminarea.'], function ($router) {
Route::post('skills-filter', 'API\Admin\SkillController#filter');
Route::resource('skills', 'API\Admin\SkillController');
...
and in app/Http/Controllers/API/Admin/SkillController.php :
<?php
namespace App\Http\Controllers\API\Admin;
use Auth;
use DB;
use Validator;
use App\User;
use App\Http\Resources\Admin\Skill as SkillResource;
class SkillController extends Controller
{
private $requestData;
private $page;
private $filter_name;
private $order_by;
private $order_direction;
public function __construct()
{
$this->middleware('jwt.auth', ['except' => []]);
$request = request();
$this->requestData = $request->all();
}
public function filter()
{
if ( ! $this->checkUsersGroups([ACCESS_ROLE_ADMIN])) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$backend_items_per_page = Settings::getValue('backend_items_per_page');
$this->page = !empty($this->requestData['page']) ? $this->requestData['page'] : '';
$this->filter_name = !empty($this->requestData['filter_name']) ? $this->requestData['filter_name'] : '';
$this->order_by = !empty($this->requestData['order_by']) ? $this->requestData['order_by'] : 'name';
$this->order_direction = !empty($this->requestData['order_direction']) ? $this->requestData['order_direction'] : 'asc';
$this->index();
$skills = Skill
::getByName($this->filter_name)
->orderBy($this->order_by, $this->order_direction)
->paginate($backend_items_per_page);
return SkillResource::collection($skills);
} // public function filter()
public function index()
{
if ( ! $this->checkUsersGroups([ACCESS_ROLE_ADMIN])) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$backend_items_per_page = Settings::getValue('backend_items_per_page');
$skills = Skill::paginate($backend_items_per_page);
return SkillResource::collection($skills);
}
As that is adminarea some filter actions can have more 1 filter...
Just interested to know which other decisions are possible here?

Passing a variable to a master template in laravel

In laravel i have defined a route like this
Route::get('/', array(){
'as' => 'index',
'uses' => 'HomeController#index'
});
The function index() in the HomeController contains
public function index(){
$index = new ExampleModel;
$data = $index->getExampleList();
return View::make('public.index');
}
Now the problem is i have a master layout called happypath inside layouts folder in my views which yields this public.index content and i need to pass this $data to layouts.happypath. How do i do this ?
You can use a view composer for example:
namespace App\Providers;
use App\ExampleModel;
use Illuminate\Support\ServiceProvider;
class ComposerServiceProvider extends ServiceProvider
{
protected $exampleModel;
public function __construct(ExampleModel $exampleModel)
{
$this->exampleModel = $exampleModel;
}
public function boot()
{
view()->composer('layouts.happypath', function ($view) {
$view->with('publicIndex', $this->exampleModel->getExampleList());
});
}
public function register()
{
//
}
}
So, every time you use/render the layouts.happypath the $publicIndex variable will be attached within the layout. Also you need to add the ComposerServiceProvider class in your config/app.php file in the providers array. You may access/reference the data using $publicIndex variable in your layout. There are other ways like global shared $information using view()->share(...) method to share a peace of data all over the views but this may help you.
I could not figure out the ComposerServiceProvider View::composer thing. So i basically solved it like this in Laravel 4.2. Added this code to the BaseController.php
protected $menuList;
public function __construct() {
$response = API::pool([
['GET', API::url('level')],
]);
$index = new Index();
$index->setCourseList($response[0]->json()['Category']);
$result = $index->getCourseList();
View::share('result', $result); //This line shares the $result globally across all the views in laravel 4.2
}
This can be done with a Service Provider. You can either use an existing one or create a new one.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\ExampleModel;
class ViewServiceProvider extends ServiceProvider
{
public function boot()
{
$index = new ExampleModel;
$data = $index->getExampleList();
view()->share('public.index', $data);
}
public function register()
{
}
}
Source: EasyLaravel.com

How to use subdomain in laravel 4?

I want to detect subdomains in routes.php in my L4 website and want to store that subdomain value somewhere so that I can access that value in each controller.
How can I do that ?
Please help
http://laravel.com/docs/routing#sub-domain-routing
Route::group(array('domain' => '{account}.myapp.com'), function() {
Route::get('user/{id}', function($account, $id) {
//
});
});
You can put this type of method in routes. However, I think it's a better fit for a filter in the 'app/filters.php' file. Try this:
Route::filter('getSubdomain', function($route, $request)
{
$host = $request->getHost();
$parts = explode('.', $host);
$subdomain = $parts[0];
// Store subdomain in session
Session::put('subdomain', $subdomain);
});
Then add the filter to the route (probably a group route) as follows:
Route::group(array('before' => 'getSubdomain'), function()
{
... add route stuff here ..
});
You can read more about how to use Laravel filters here:
http://laravel.com/docs/routing#route-filters
You can use Request to get your domain anywhere.
So, create a BaseController and add a method to get the current domain on all your extended controllers:
class BaseController extends Controller {
public function getDomain()
{
return Request::getHost();
}
}
And use it:
class PostsController extends BaseController {
public function store()
{
$post = new Post;
$post->domain_id = Domain::where('name', $this->getDomain())->first()->id;
$post->save();
}
}
Of course, this controller example supposes that you have a Domain model:
class Domain extends Eloquent {
private $table = 'domains';
}
EDIT:
Unless you have a very good reason for it, you don't have to use routes or store your subdomain on a session, unless you have a really good reason for this, it's a smell. Take a look at Laravel's code, there only one session stored by it: Laravel's session.
You can create a helper function:
Create a app/helpers/functions.php file (this is just an example) and add this helper function there:
function getCurrentSubdomain()
{
$domain = Config::get('app.domain');
preg_match("/^(.*)(\.$domain)$/", Request::getHost(), $parts);
return $parts[1];
}
Open your app/config/app.php and add a domain configuration to it:
return array(
'domain' => 'myapp.com',
...
);
Add the file to the autoload section of your composer.json:
"autoload": {
"classmap": [
...
],
"files": [
"app/helpers/functions.php"
]
},
Then you can use it everywhere: controllers, classes, routers, etc. Here's the same example as before, using it:
class PostsController extends BaseController {
public function store()
{
$post = new Post;
$post->domain_id = Domain::where('name', getCurrentSubdomain())->first()->id;
$post->save();
}
}
You can also create a class and a Facade for it, so you can call this class from anywhere, like Laravel does:
Helper::getCurrentSubdomain();
Or you can do the same by just creating a class and create a static function (less testable).
This is working on my Laravel 4.2 right now.
On your routes file:
Route::group(['domain' => '{subdomain}.myapp.com'], function()
{
Route::get('/', function($subdomain)
{
return "Your subdomain is: ".$subdomain;
});
});
Get subdomain in your controllers or anywhere:
$subdomain = Route::getCurrentRoute()->getParameter('subdomain');

Resources