In default the ordering of left menu items is in alphabetical order.
My client wants to order those menus manually. Any idea how to make it possible?
Go to answer
You can do it in
App\Providers\NovaServiceProvider.php
add a method resources() and register the resources manually like
protected function resources()
{
Nova::resources([
User::class,
Post::class,
]);
}
Alternate
There is another way mentioned in this gist, this seems good too, but official documentation has no mention of it yet.
Resource
<?php
namespace App\Nova;
class User extends Resource
{
/**
* The model the resource corresponds to.
*
* #var string
*/
public static $model = 'App\\User';
/**
* Custom priority level of the resource.
*
* #var int
*/
public static $priority = 1;
// ...
}
and in NovaServiceProvider
<?php
namespace App\Providers;
use Laravel\Nova\Nova;
use Laravel\Nova\Cards\Help;
use Illuminate\Support\Facades\Gate;
use Laravel\Nova\NovaApplicationServiceProvider;
class NovaServiceProvider extends NovaApplicationServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
Nova::sortResourcesBy(function ($resource) {
return $resource::$priority ?? 99999;
});
}
}
This way you set priority of resource and based on priority you render the resource.
A cleaner way and tested on latest Nova 3.x. Also, this has been added to Nova since version 2.10+ All you need to do is add a static property on your nova classes. For example Clients will be:
/**
* The side nav menu order.
*
* #var int
*/
public static $priority = 2;
Then after that you can use the NovaServiceProvider to tell nova to use your custom ordering. You can place the code in the boot method
public function boot()
{
Nova::sortResourcesBy(function ($resource) {
return $resource::$priority ?? 9999;
});
}
**Reference Nova Private Repo
There are two ways to achieve this:
By setting priority to Resource
Ordering Resource models in NovaServiceProvider
1. Priority Method
Add priority as in the following code in Resource model:
public static $priority = 2;
Then update NovaServiceProvider like this:
public function boot()
{
Nova::sortResourcesBy(function ($resource) {
return $resource::$priority ?? 9999;
});
}
2. Ordering Resource models in NovaServiceProvider
In NovaServiceProvider, order Resource models like this:
protected function resources()
{
Nova::resources([
User::class,
Post::class,
]);
}
you can use grouping if that helps. I know it's not a 100% fix but maybe it will help a bit.
public static $group = 'Admin';
Change /nova/resources/navigation.blade.php {{ $group }} to following:
{!! $group !!}
Now you can easily sort the groups as follows:
public static $group = '<span class="hidden">20</span>Music';
or
public static $group = '<span class="hidden">30</span>User';
Attention: You must convert special characters in the title!
With the links, it's a bit other....
First Method: dirty and ugly
You can change
{{ $resource::label() }}
to
{{ substr($resource::label(), 1) }}
Then you can sort the links by the first letter of the resource name.
AUser
BAlbum
CContact
Or a better Method for Links
crate app/Nova/CustomResource.php:
<?php
namespace App\Nova;
use Illuminate\Support\Str;
abstract class CustomResource extends Resource
{
public static $label = '';
/**
* #return string
*/
public static function label()
{
if(static::$label) {
return static::$label;
}
return Str::plural(Str::title(Str::snake(class_basename(get_called_class()), ' ')));
}
}
Change /nova/resources/navigation.blade.php
{!! $resource::label() !!}
And in the Nova resource, extends this custom resource and You can use public static $label:
class Lyric extends CustomResource
{
public static $label = '<span class="hidden">10</span>Lyrics';
public static function singularLabel()
{
return __('Lyric');
}
Attention: You must convert special characters in the title!
to sort the groups:
add this to your resources:
public static function groupOrder() {
return 9999999;
}
you can overwrite it by adding it to any member resource to downgrade it's order in the navigation tree:
public static function groupOrder() {
return 5;
}
add this before returning at the end of resourcemanager (i hope i shouldn't have to overwrite this at this place):
$arrSort = [];
foreach ($navigation as $group => $resources) {
$resourcesGruoupOrders = [];
foreach ($resources as $aResource) {
$resourcesGruoupOrders[] = $aResource::groupOrder();
}
$arrSort[] = min($resourcesGruoupOrders);
}
$navigation = json_decode(json_encode($navigation), true);
array_multisort($navigation, SORT_ASC, SORT_NUMERIC, $arrSort);
If You wondering how to sort groups using a custom sort algorithm here is the clean solution.
In NovaServiceProvider in boot() method just add a custom callback.
$order = array_flip(['Modules', 'Localization', 'Other', 'Settings']);
Nova::mainMenu(static function (Request $request, Menu $menu) use ($order): Menu {
$resources = $menu->items->firstWhere('name', 'Resources');
$resources->items = $resources->items->sort(
fn (MenuGroup $a, MenuGroup $b): int => ($order[$a->name] ?? INF) <=> ($order[$b->name] ?? INF)
);
return $menu;
});
Using $order array you can easily control the position of every specific group. Groups that are not included in this array will be moved to the end of the menu. This behavior can be changed to a moving to the beginning by replacing INF with -INF.
Before add to resource static property
public static $priority = 1;
Then in NovaServiceProvider replace resource method
protected function resources()
{
$namespace = app()->getNamespace();
$resources = [];
foreach ((new Finder)->in(app_path('Nova'))->files() as $resource) {
$resource = $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($resource->getPathname(), app_path().DIRECTORY_SEPARATOR)
);
if (is_subclass_of($resource, Resource::class) &&
! (new \ReflectionClass($resource))->isAbstract()) {
$resources[] = $resource;
}
}
Nova::resources(
collect($resources)->sort(function ($a, $b) {
return $a::$priority > $b::$priority;
})->all()
);
}
Related
Currently working with Symfony 5.2
I am trying to create a dynamic route for multiple enties which have mostly same properties (some still have other fields too, but all have the same default properties).
Example (minified)
Entity News:
- id, title, author
Entity Event:
- id, title, author
I am now trying to create a dynamic route to fetch News or Events from my the repository.
class ContentController extends AbstractController {
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type) { //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository(/* entity class */)->findAll();
}
}
I already came up with some ideas, but not sure if they are good practise.
1) use full namespace for the entity (how do I check if the class exists? error handling?)
$results = $this->getDoctrine()->getRepository('App\\Entity\\News')->findAll();
$results = $this->getDoctrine()->getRepository('App\\Entity\\' . ucfirst($type))->findAll();
2) provide a route for each entity and call a generic function (not exactly what I want because I have like 10+ entities for this)
class ContentController extends AbstractController {
/**
* #Route("/news", name="get_news")
*/
public function get_news() {
$results = getContent(News::class);
}
/**
* #Route("/event", name="get_event")
*/
public function get_event() {
$results = getContent(Event::class);
}
public function getContent($class) {
return $this->getDoctrine()->getRepository($class)->findAll();
}
}
Mabye some of you have better ideas/improvements and can help me out a bit.
You can define available for fetching entities manually.
class ContentController extends AbstractController
{
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type)
{ //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository($this->getEntityClassFromType($type))->findAll();
}
private function getEntityClassFromType(string $type): string
{
//define your entities manually
foreach ([News::class, Event::class] as $class) {
$parts = explode('\\', $class);
$entity = array_pop($parts);
if ($type === lcfirst($entity)) {
return $class;
}
}
throw new NotFoundHttpException();
}
}
Or check your entities dynamically using $entityManager->getMetadataFactory()->hasMetadataFor($className);
I'd like to define a "global" method that can be used by multiple controllers and commands. Where should it be placed in Laravel 5.4?
Let's say I have the following controller. How would I call the "global" method instead, and where would that "global" method be located exactly?
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Flight;
class FlightsController extends Controller
{
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Index
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$flights = Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
}
When you want a method that fetches many models, and you want to use it in many places, put it in a Repository:
class FlightRepository
{
public function getLastTenFlights()
{
return Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
}
}
For example from your controller:
public function index( FlightRepository $repo )
{
$flights = $repo->getLastTenFlights();
//if you want you can put this additional login in the method too...
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
You can create a Object and call the object when you want.
See example:
FlighRepository = new FlighRepository;
FlighRepository->index();
I personally prefer query scopes to repositories, so I would do something like this:
class Flight extends Model
{
// model setup
/**
* Scope query to get last 10 flights.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeLastTen($query)
{
return $query->where('active', 1)->orderBy('name', 'desc')->take(10);
}
// rest of model
}
And you can use it similarly to how you're currently using it, only it's more readable:
$flights = Flight::lastTen()->get();
This also has the advantage of being able to chain other queries off of it. Say, for example, you wanted the last ten American Airlines flights, you could do:
$flights = Flight::lastTen()->where('airline', 'American')->get();
// equivalent to
// $flights = Flight::where('airline', 'American')->lastTen()->get();
I think that service is the best option to store the functionality which is shared between controllers and commands. You can access them using Service Container (https://laravel.com/docs/5.5/container).
What I'm trying to do is to append the comments of each article to the articles object, but the problem is that I need to request different number of comments each time.
and for some reason I need to use mutators for that, because some times I request 50 articles and I don't want to loop through the result and append the comments.
So is it possible to do something like the following and how to pass the extra argument.
This the Model:
class Article extends Model
{
protected $appends = ['user', 'comments', 'media'];
public function getCommentsAttribute($data, $maxNumberOfComments = 0)
{
// I need to set maxNumberOfComments
return $this->comments()->paginate($maxNumberOfComments);
}
}
Here is the controller:
class PostsController extends Controller
{
public function index()
{
//This will automatically append the comments to each article but I
//have no control over the number of comments
$posts = Post::user()->paginate(10);
return $posts;
}
}
What I don't want to do is:
class PostsController extends Controller
{
public function index()
{
$articles = Post::user()->all();
$number = 5;
User::find(1)->articles()->map(function(Article $article) {
$article['comments'] = $article->getCommnets($number);
return $article;
});
return Response::json($articles);
}
}
Is there a better way to do it? because I use this a lot and it does not seams right.
Judging from the Laravel source code, no – it's not possible to pass an extra argument to this magic accessor method.
The easiest solution is just to add another, extra method in your class that does accept any parameters you wish – and you can use that method instead of magic property.
Eg. simply rename your getCommentsAttribute() to getComments() and fire ->getComments() instead of ->comments in your view, and you are good to go.
I just set a public property on the model. At the accessing point, I update that property to my desired value. Then, in the attribute method, I read the desired arguments from that property. So, putting all of that together,
// Model.php
public $arg1= true;
public function getAmazingAttribute () {
if ($this->arg1 === false)
$this->relation()->where('col', 5);
else $this->relation()->where('col', 15);
}
// ModelController.php
$instance->arg1 = false;
$instance->append('amazing');
It is been a while for this question, but maybe someone will need it too.
Here is my way
{
/**
* #var string|null
*/
protected ?string $filter = null;
/**
* #return UserSettings[]|null
*/
public function getSettingsAttribute(): ?array
{
return services()->tenants()->settings($this)->getAll();
}
/**
* #return FeatureProperty[]|null
*/
public function getFeaturePropertiesAttribute(): ?array
{
return services()->tenants()->featureProperty($this)->getListByIds($this->filter);
}
/**
* #param string|null $filter
* #return Tenant
*/
public function filter(string $filter = null): Model
{
$this->filter = $filter;
return $this;
}
Accessor is using some service to get values. Service accepts parameters, in my case string, that will be compared with featureProperty->name
Magic happens when you return $this in filter method.
Regular way to call accessor would be:
$model->feature_properties
Extended way:
$model->filter('name')->feature_properties
Since filter argument can be null, we can have accessor like this:
$filter = null
$model->filter($filter)->feature_properties
In case you would like to play with it a little more you can think about overriding models getAttribute or magic __call methods implementing filter in manner which will be similar to laravel scopes
I know its an old question, but there is another option, but maybe not the best:
$articles = Post::user()->all();
$number = 5;
$articles->map(function($a) use($number){
$a->commentsLimit = $number;
return $a;
});
And then in getCommentsAttribute():
return $this->comments()->paginate($this->commentsLimit);
Within Laravel you can easily define abilities and then hook into them later on a user request regarding to do different actions:
$gate->define('update-post', function ($user, $post) {
return $user->id === $post->user_id;
});
But almost all my defined abilities has this part $user->id === $model->user_id in it. I don't like it as it's a kind of repeating a condition over and over which I think could be more abstract.
Most of my defined abilities are according to updating/deleting records, so it would be better if I could make a global condition applied to all of them or if there could be a group ability defining which is like to what we do in routing.
Is there any workaround for it? I really like it DRY.
Everything in Laravel is extendable, that's the power of its service providers.
You can extend the Gate object to a MyCustomGate object and do whatever you want in that object. Here's an example:
MyCustomGate.php
class MyCustomGate extends \Illuminate\Auth\Access\Gate
{
protected $hasOwnershipVerification = [];
/**
* Define a new ability.
*
* #param string $ability
* #param callable|string $callback
* #return $this
*
* #throws \InvalidArgumentException
*/
public function defineWithOwnership($ability, $callback, $foreignUserIdKey = "user_id")
{
// We will add this
$this->hasOwnershipVerification[$ability] = $foreignUserIdKey;
return $this->define($ability, $callback);
}
/**
* Resolve and call the appropriate authorization callback.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback(
$user, $ability, $arguments
);
// We will assume that the model is ALWAYS the first key
$model = is_array($arguments) ? $arguments[0] : $arguments;
return $this->checkDirectOwnership($ability, $user, $model) && call_user_func_array(
$callback, array_merge([$user], $arguments)
);
}
/**
* Check if the user owns a model.
*
* #param string $ability
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param \Illuminate\Database\Eloquent\Model $model
* #return bool
*/
protected function checkDirectOwnership($ability, $user, $model)
{
if(!isset($this->hasOwnershipVerification[$ability])) {
return true
}
$userIdKey = $this->hasOwnershipVerification[$ability];
// getAuthIdentifier() is just ->id, but it's better in case the pk of a user is different that id
return $user->getAuthIdentifier() == $model->{$userIdKey};
}
}
Then, you will have to tell Laravel to use your gate instead of the default one. You ca do that in your AuthServiceProvider (assuming that it's extending Illuminate\Auth\AuthServiceProvider, just add the following method.
AuthServiceProvider
/**
* Register the access gate service.
*
* #return void
*/
protected function registerAccessGate()
{
$this->app->singleton(\Illuminate\Contracts\Auth\Access\Gate::class, function ($app) {
return new MyCustomGate($app, function () use ($app) {
return $app['auth']->user();
});
});
}
And this way, you can define abilities using defineWithOwnership() method instead of define(). You can still use define() for abilities that don't require ownership verification. There's a third parameter defineWithOwnership() accepts which is $foreignUserIdKey; that's used for the case when a model has a different field for the user id.
Note: I wrote the code on the fly and did not try it, it may have errors, but you get the idea.
I checked your question quite a bit, but I've found no "easy" way to do it.
Instead, what I would probably do is this:
<?php
namespace App\Policies;
use App\User;
use App\Post;
trait CheckOwnership {
protected function checkOwnership($user, $model) {
$owned = $user->id === $model->user_id;
if ($owned === false)
throw new NotOwnedException;
}
}
class PostPolicy
{
use CheckOwnership;
public function update(User $user, Post $post)
{
try {
$this->checkOwnership($user, $post);
//continue other checks
} catch (NotOwnedException $ex) {
return false;
}
}
}
Add this function to your AuthServiceProvider
public function defineAbilities(array $abilities, $gate)
{
foreach($abilities as $name => $model){
$gate->define($name, function ($user, $model){
return $user->id === ${$model}->user_id;
});
}
}
and then inside boot method
$this->defineAbilities(['ability1' => 'model1', 'ability2' => 'model2'], $gate);
You can define another function and call it within the anonymous function. This will allow you to have commonly-used code in one central location while still allowing any resource-specific logic.
Add this function to your AuthServiceProvider class:
public function userCheck(User $user, $target)
{
// do the user id check
$result = isset($target->user_id) && isset($user) && $user->id === $target->user_id;
return $result;
}
Your code, modified:
$gate->define('update-post', function ($user, $post) {
// call the function
$result = $this->userCheck($user, $post);
// do some kind of 'update-post' specific check
return $result/* && some_bool_statement*/;
});
I think you can use middlewares.
Simply make a admin middleware and use it in your routes and routes group.
And there is no security bug on your project (delete, create & ... actions) because Laravel has csrf token!
You can use before() function, also.
And then an important note:
if you don't define a correspond function on Policy class and call it $this->authorize($post) on a controller an unauthorized Action error will be thrown unless before()methodreturnstrue.
for example call $this->authorize on Dashboard\PostsController:
public function edit($id)
{
$post = Post::find($id)->first();
$this->authorize($post);
return view('dashboard.post')->with(compact('post'));
}
and if we defined a PostPolicy Class:
class PostPolicy
{
use HandlesAuthorization;
public function before($user, $ability)
{
return $user->is_admin;
}
}
If user be admin he/she can edit post because we returned true in before() method despite of have not a method with same name (as edit method in PostsController).
In fact Laravel will check for before method mthod on Policy Class. if before return'snull will check for correspond method with same name on controller method and if this method not found user cannot perform action.
Thank you laravel for DRY us!♥
Is there a way to get a pagination pretty URL in Laravel 4?
For example, by default:
http://example.com/something/?page=3
And what I would like to get:
http://example.com/something/page/3
Also, the pagination should render this way, and appending to the pagination should appear in this way.
Here's a hacky workaround. I am using Laravel v4.1.23. It assumes page number is the last bit of your url. Haven't tested it deeply so I'm interested in any bugs people can find. I'm even more interested in a better solution :-)
Route:
Route::get('/articles/page/{page_number?}', function($page_number=1){
$per_page = 1;
Articles::resolveConnection()->getPaginator()->setCurrentPage($page_number);
$articles = Articles::orderBy('created_at', 'desc')->paginate($per_page);
return View::make('pages/articles')->with('articles', $articles);
});
View:
<?php
$links = $articles->links();
$patterns = array();
$patterns[] = '/'.$articles->getCurrentPage().'\?page=/';
$replacements = array();
$replacements[] = '';
echo preg_replace($patterns, $replacements, $links);
?>
Model:
<?php
class Articles extends Eloquent {
protected $table = 'articles';
}
Migration:
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration {
public function up()
{
Schema::create('articles', function($table){
$table->increments('id');
$table->string('slug');
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
public function down()
{
Schema::drop('articles');
}
}
It's possible but you need to code a bit.
First you need to change in app/config/app.php pagination service provider - you need to write your own.
Comment:
// 'Illuminate\Pagination\PaginationServiceProvider',
and add
'Providers\PaginationServiceProvider',
in providers section.
Now you need to create your PaginationServiceProvider to use custom pagination factory:
model/Providers/PaginationServiceProvider.php file:
<?php
namespace Providers;
use Illuminate\Support\ServiceProvider;
class PaginationServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app->bindShared('paginator', function ($app) {
$paginator = new PaginationFactory($app['request'], $app['view'],
$app['translator']);
$paginator->setViewName($app['config']['view.pagination']);
$app->refresh('request', $paginator, 'setRequest');
return $paginator;
});
}
/**
* Get the services provided by the provider.
*
* #return array
*/
public function provides()
{
return array('paginator');
}
}
Above you create Providers\PaginationFactory object, so now we need to create this file:
model/providers/PaginationFactory.php file:
<?php
namespace Providers;
use Illuminate\Pagination\Factory;
class PaginationFactory extends Factory {
/**
* Get a new paginator instance.
*
* #param array $items
* #param int $total
* #param int|null $perPage
* #return \Illuminate\Pagination\Paginator
*/
public function make(array $items, $total, $perPage = null)
{
$paginator = new \Utils\Paginator($this, $items, $total, $perPage);
return $paginator->setupPaginationContext();
}
}
Here you create only \Utils\Paginator object so now let's create it:
model/Utils/Paginator.php file:
<?php
namespace Utils;
class Paginator extends \Illuminate\Pagination\Paginator {
/**
* Get a URL for a given page number.
*
* #param int $page
* #return string
*/
public function getUrl($page)
{
$routeParameters = array();
if ($page > 1) { // if $page == 1 don't add it to url
$routeParameters[$this->factory->getPageName()] = $page;
}
return \URL::route($this->factory->getCurrentUrl(), $routeParameters);
}
}
In this file we finally override default method for creating pagination urls.
Let's assume you have route defined this way:
Route::get('/categories/{page?}',
['as' => 'categories',
'uses' => 'CategoryController#displayList'
])->where('page', '[1-9]+[0-9]*');
As you see we defined here route name using as (it's important because of Paginator implementation above - but you can do it of course in different way).
Now in method displayList of CategoryController class you can do:
public function displayList($categories, $page = 1) // default 1 is needed here
{
Paginator::setCurrentPage($page);
Paginator::setBaseUrl('categories'); // use here route name and not the url
Paginator::setPageName('page');
$categories = Category::paginate(15);
return View::make('admin.category')->with(
['categories' => $categories]
);
}
When in your view you add:
<?php echo $categories->links(); ?>
you will get generated urls this way:
http://localhost/categories
http://localhost/categories/2
http://localhost/categories/3
http://localhost/categories/4
http://localhost/categories/5
without ? in query string
However in my opinion something like this should be added by default or at least it should be enough to extend one class and not to create 3 classes just to implement one method.
hope this is helpful for someone, I've made a trait to be used in models.
The idea is that this custom method can detect current route and adjust links to use correct segment position for {page} parameter:
https://gist.github.com/zofe/ced0054e6ac6eff1ea95
For Laravel 5.8 use this solution in blade.php where you generate links:
$links = $data->links();
$patterns = '#\?page=#';
$replacements = '/page/';
$one = preg_replace($patterns, $replacements, $links);
$pattern2 = '#page/([1-9]+[0-9]*)/page/([1-9]+[0-9]*)#';
$replacements2 = 'page/$2';
$paginate_links = preg_replace($pattern2, $replacements2, $one);
echo $paginate_links;
The only way I can think of doing this is by extending the Paginator class to do the matching. However, just know that it may conflict with third-party packages and other classes/libraries. The current method is designed to work with nearly all classes/libraries/packages.
Perhaps you could try the following:
http://packalyst.com/packages/package/desmart/pagination ('pagination' by 'desmart')
For anyone that is using laravel version 5.6+
You can pass additional parameters to set the page number.
According to: https://laravel.com/api/5.6/Illuminate/Database/Eloquent/Builder.html#method_paginate
Example:
StoreController.php
/**
* Show sale item based on given page
*
* #param int $page
* #return \Illuminate\Http\Response
*/
public function showPage($page = 1)
{
$saleItems = SaleItem::paginate(10, array('*'), 'page', $page);
...
}
Then, in your blade template. You can just route( ... , array('page' => $page));