Laravel Nova + Spatie Media library - laravel

Trying to use Laravel Nova with Spatie Media Library. I created upload field like this:
Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
}),
Seams ok, but Nova still trying to save file name to "logo" column in manufacturers table.
Original excample to customize this field was:
File::make('Attachment')
->store(function (Request $request, $model) {
return [
'attachment' => $request->attachment->store('/', 's3'),
'attachment_name' => $request->attachment->getClientOriginalName(),
'attachment_size' => $request->attachment->getSize(),
];
})

I found a work around by setting an empty mutator on the model. In your case it would be:
class Manufacturer extends Model implements HasMedia
{
use HasMediaTrait;
public function setLogoAttribute() {}
//...
}
Here's an example of my entire implementation. Note that currently with Nova 1.0.6, the preview() method is not working, it's returning the thumbnail() url.
App/GalleryItem
class GalleryItem extends Model implements HasMedia
{
use HasMediaTrait;
public function setImageAttribute() {}
public function registerMediaConversions(Media $media = null)
{
$this->addMediaConversion('thumbnail')
->fit(Manipulations::FIT_CROP, 64, 64);
$this->addMediaConversion('preview')
->fit(Manipulations::FIT_CROP, 636, 424);
$this->addMediaConversion('large')
->fit(Manipulations::FIT_CONTAIN, 1920, 1080)
->withResponsiveImages();
}
public function registerMediaCollections()
{
$this->addMediaCollection('images')->singleFile();
}
}
App/Nova/GalleryItem
class GalleryItem extends Resource
{
public static $model = 'App\GalleryItem';
public static $with = ['media'];
public function fields(Request $request)
{
return [
Image::make('Image')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('image')->toMediaCollection('images');
})
->preview(function () {
return $this->getFirstMediaUrl('images', 'preview');
})
->thumbnail(function () {
return $this->getFirstMediaUrl('images', 'thumbnail');
})
->deletable(false);
];
}
}

As with Nova 3 (and Laravel 8) you need to return true from the fillUsing or store method:
File::make('Attachment')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return true;
// This will tell nova that you have taken care of it yourself.
})
As soon as you return anything but true nova will assume, that it needs to save something to the database. This leads to an error if the field does not exist in db (as to expect with spatie-medialibrary) or it will overwrite your precious data if the field exists but serves another purpose.

Nova allows you to return true from the callback to indicate that the processing is complete and that it shouldn't set any attributes itself.
This is the code that runs the callback:
protected function fillAttribute(NovaRequest $request, $requestAttribute, $model, $attribute)
{
//...
$result = call_user_func($this->storageCallback, $request, $model);
if ($result === true) {
return;
}
if (! is_array($result)) {
return $model->{$attribute} = $result;
}
foreach ($result as $key => $value) {
$model->{$key} = $value;
}
}
So either true or any empty array will achieve the same thing, but personally feels clearer to do the former.

Image::make('Logo')
->store(function (Request $request, $model) {
$model->addMediaFromRequest('logo')->toMediaCollection('manufacturers');
return [];
}),
Maybe returning an empty array prevent nova from saving the name.

Related

Laravel: prevent changing other users' items

I have three models. I want to avoid that users can change the todo's from todolists belonging to other users.
class User extends Authenticatable
{
public function todolists()
{
return $this->hasMany('App\Todolist');
}
public function todos()
{
return $this->hasManyThrough('App\Todo', 'App\Todolist');
}
}
class Todolist extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
public function todos()
{
return $this->hasMany('App\Todo');
}
}
class Todo extends Model
{
protected $casts = [
'completed' => 'boolean',
];
public function todolist()
{
return $this->belongsTo('App\Todolist');
}
}
To avoid users can view other users' todolists and todo items, I have implemented the following:
public function getTodosForTodolist(Todolist $todolist)
{
if (Auth::user()->id == $todolist->user_id) {
$todos = Todo::where('todolist_id', $todolist->id )->get();
return view('todo/index', ['todos' => $todos);
}
else {
abort(403, 'Unauthorized action.');
}
}
Next step is to prevent that users can edit other users' todo items. Currently in the TodoController I have simply the following:
public function edit(Todo $todo)
{
if (Auth::user()->todos->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
}
This gives the following error:
Property [id] does not exist on this collection instance.
The error is because the current user has multiple todos. So I changed my code as follows.
public function edit(Todo $todo)
{
if (Auth::user()->todos->first()->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
abort('403', 'Unauthorized action.');
}
This works but it just feels very wrong to do this as such.
What would be a better way to accomplish that users' can view/edit/delete items belonging to other users?
I suggest that you use policies for your Todo and TodoList models and a scope to restrict todos to one user to prevent duplicated code within your app:
class ToDoListPolicy
{
public function view(User $user, TodoList $post)
{
return $user->id === $todolist->user_id;
}
}
class ToDoPolicy
{
public function edit(User $user, Todo $toDo)
{
$toDo->loadMissing('todolist');
return $user->id === $toDo->todolist->user_id;
}
}
Register them in your AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
TodoList::class => ToDoListPolicy::class,
Todo::class => ToDoPolicy::class
];
}
and then use them in your actions:
public function getTodosForTodolist(Todolist $toDoList)
{
$this->authorize('view', $toDoList);
$toDoList->loadMissing('todos');
return view('todo.index', ['todos' => $toDoList->todos);
}
class ToDoController extends Controller
{
public function edit(Todo $toDo)
{
$this->authorize('edit', $toDo);
return view('todo.edit', compact('toDo'));
}
}
And a scope to restrict the query to a specific user:
class Todo extends Model {
// ...
public function scopeByUser(Builder $query, ?User $user = null)
{
if (! $user) {
$user = Auth::user();
}
$query->whereHas('todolist', function (Builder $toDoListQuery) use ($user) {
$toDoListQuery->where('user_id', $user->id);
});
}
}
Answer to your questions in the comments.
Q1: I had to put Auth::user()->can('view', $todolist); in an if-else clause for it to work. Guess this is the way it works?
Q2: what is the difference between $this->authorize('edit', $todo) and Auth::user()->can('edit', $todo)?
Sorry, that was a mistake on my side. Auth::user()->can() returns a boolean whereas $this->authorize() (which is a method of the AuthorizesRequests trait usually included in the BaseController) throws an exception if the authorization failed.
If you want to let each user work only with his/her own Todos then adding a Global Scope is what you are looking for. This implementation will let your application feel that Todos ( of users other than the logged one ) does not exist.
Global Scopes can be used for many models which means it will reduce boiler plate code.
https://laravel.com/docs/7.x/eloquent#global-scopes

Create new Post with default Category belongsToMany

I have a Post/Category manyToMany relations and would like to be able to attach a default category named "Uncategorised" to each new post that is created. How can I do that? A BelongsToMany method only works on the Details page, not on Create page.
BelongsToMany::make(__('Categories'), 'categories', Category::class),
You can also set default value to your database field so that you can omit passing category and will be taken default to Uncategorised like if you are using MySQL you can do it this way by creating migration
$table->text('category')->default(0);
Because the BelongsToMany not show on mode create in Post Nova model. So we have to make our custom Select, by add this code to your fields:
public function fields(Request $request)
{
if($request->editMode=="create"){
$categories = \App\Category::get(['id','name']);
$options = [];
foreach($categories as $value){
$options[$value->id] = $value->name;
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
Select::make('Categories', 'category_id')
->options($options)
->displayUsingLabels()
->withMeta(['value' => 1]) // 1 = id of Uncategorised in categories table
];
}
return [
ID::make()->sortable(),
Text::make('Title'),
Text::make('Summary'),
Textarea::make('Content'),
BelongsToMany::make('Categories','categories')->display('name'),
];
}
Don’t forget relationship function in both, Post and Category model:
class Post extends Model
{
public function categories(){
return $this->belongsToMany(Category::class, 'category_post', 'post_id', 'category_id');
}
}
And:
class Category extends Model
{
public function posts(){
return $this->belongsToMany(Post::class,'category_post', 'category_id', 'post_id');
}
}
Then, custom the function process the data on mode Create of Post resource page, it’s at nova\src\Http\Controllers\ResourceStoreController.php, change function handle to this:
public function handle(CreateResourceRequest $request)
{
$resource = $request->resource();
$resource::authorizeToCreate($request);
$resource::validateForCreation($request);
$model = DB::transaction(function () use ($request, $resource) {
[$model, $callbacks] = $resource::fill(
$request, $resource::newModel()
);
if ($request->viaRelationship()) {
$request->findParentModelOrFail()
->{$request->viaRelationship}()
->save($model);
} else {
$model->save();
// your code to save to pivot category_post here
if(isset($request->category_id)&&($resource=='App\Nova\Post')){
$category_id = $request->category_id;
$post_id = $model->id;
\App\Post::find($post_id)->categories()->attach($category_id);
}
}
ActionEvent::forResourceCreate($request->user(), $model)->save();
collect($callbacks)->each->__invoke();
return $model;
});
return response()->json([
'id' => $model->getKey(),
'resource' => $model->attributesToArray(),
'redirect' => $resource::redirectAfterCreate($request, $request->newResourceWith($model)),
], 201);
}
}
All runs well on my computer. A fun question with me! Hope best to you, and ask me if you need!
What I ended up doing was saving the data on Post Model in boot().
public static function boot()
{
parent::boot();
static::created(function (Post $post) {
$post->categories()->attach([1]);
});
}

Laravel json response returns encrypted data

I'm using an Encryptable trait to encrypt my data for the a Room model.
RoomController (/rooms) returns the decrypted data but ApiRoomController (/api/rooms) does not.
How could I make it returns the decrypted data?
Encryptable Trait
trait Encryptable
{
public function getAttribute($key)
{
$value = parent::getAttribute($key);
if (in_array($key, $this->encryptable) && $value !== '' && $value !== null ) {
$value = Crypt::decrypt($value);
}
return $value;
}
public function setAttribute($key, $value)
{
if (in_array($key, $this->encryptable)) {
$value = Crypt::encrypt($value);
}
return parent::setAttribute($key, $value);
}
}
RoomController index function
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return view('rooms.index')->withRooms($rooms);
}
ApiRoomController index function
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return response()->json($rooms);
}
I found a way using API Resources:
php artisan make:resource Rooms --collection
Then in your app/Http/Resources/Rooms.php file:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
// more fields here
];
}
Then in your ApiRoomController.php file:
use App\Http\Resources\Rooms;
public function index()
{
$rooms = Room::select('id', 'name')->get()->sortBy('name')->values()->all();
return Rooms::collection($rooms);
}
Seems like #emotality came up with a good solution for this already...
However, the reason for this not working as you expected is because the underlying Model's toArray() / toJson() methods do not call the getAttribute() method in your trait.
This is important because the response()->json() method maps the given collection and calls the toJson() method on each model in order to prepare it for a response.
Therefore, you can also solve this by overwriting the toArray method in your model.
class Room extends Model
{
use Encryptable;
public function toArray()
{
return [
'id' => $this->id,
'name' => $this->name,
// ...
];
}
}

How to call a function in one controller to the other controller in laravel

I want to call a function in another controller. when i call this gives me an error.
Call to undefined method Illuminate\Database\Query\Builder::defaultBuckets()
I dont know why it gives me this error. I don't know i am calling this function rightly in another controller. Here is my code. Please Help.
Here is my function i created in my BucketController:
public function defaultBuckets()
{
$buckets = Bucket::where('bucket_type', 'default')->get();
}
And here is my Profile controller function Where i call this function:
public function show(User $user)
{
$authUser = JWTAuth::parseToken()->toUser();
if (! $user->isBlocking($authUser) && ! $user->isBlockedBy($authUser)) {
if($authUser->id == $user->id){
$profile = $user->where('id', $user->id)->defaultBuckets()->with([
'posts', 'likes', 'followers', 'following'])->first();
} else{
$profile = $user->where('id', $user->id)->with([
'posts' => function ($query) {
$query->where('post_type', 'public');
},
'buckets' => function ($query) {
$query->where('bucket_type', 'public');
},
'likes' => function ($query) {
$query->where('post_type', 'public');
},
'followers', 'following'])->first();
}
return response()->json(['profile'=> $profile], 200);
}
return response()->json(['message'=> 'Your are not able to open profile of this user'], 200);
}
I Think there is mistake. You said you have this function in your BucketController
public function defaultBuckets()
{
$buckets = Bucket::where('bucket_type', 'default')->get();
}
and then you are firing the function from user model in your ProfileController
$profile = $user->where('id', $user->id)->defaultBuckets()->with([
'posts', 'likes', 'followers', 'following'])->first();
That is the reason it says that there is no function named "defaultBuckets".
You have to put this function in your User model and everything will work fine.
Also don't forget to return the buckets as well like this:
To return all buckets
public function defaultBuckets()
{
$buckets = Bucket::where('bucket_type', 'default')->get();
return $buckets; // all buckets
}
To return a user's buckets only
public function defaultBuckets()
{
return $this->hasMany(Bucket::class)->where('bucket_type', 'default');
}
Make sure to accept the relationship from user in bucket model like this:
public function user(){
return $this->hasOne(User::class, 'bucket_id' , 'user_id');
}
You can replace column names (bucket_id,user_id) according to your database.
Let me know if this fixes your problem

Policy in Laravel 5.6

I'm trying to have access level control through policy in my Laravel 5.6 application.
I have a Subscriber model and a Company model, Subscribers are only given access to Company by there office locations according to states/region, i.e. a subscriber can view the details of the office if it belongs to the region being assigned to them. for this I have models:
Subscriber
class Subscriber extends Model {
//Fillables and basic attributes being assigned
public function stateIncludeRelation()
{
return $this->belongsToMany('Models\State','subscriber_states',
'subscriber_id', 'state_id');
}
public function user()
{
return $this->belongsTo('Models\User', 'user_id', 'id');
}
}
Company
class Company extends Model {
//Fillables and basic attributes being assigned
public function offices()
{
return $this->hasMany('Models\Company\Office', 'company_id');
}
}
then for Office
class Office extends Model {
//Fillables and basic attributes being assigned
public function company()
{
return $this->belongsTo('Models\Company', 'company_id', 'id');
}}
}
And a common State table:
class State extends Model {
//Fillables and basic attributes being assigned
public function subscriberAccess()
{
return $this->belongsToMany('Models\Subscriber',
'subscriber_states_included_relation',
'state_id', 'subscriber_id');
}
public function companyOffice()
{
return $this->hasOne('Models\Company\Office', 'state', 'id');
}
}
I created a CompanyPolicy something like this:
class CompanyPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view the subscriber.
*
* #param User $user
* #param Company $company
* #return mixed
*/
public function view(User $user, Company $company)
{
//Finding subscriber/user state
$userState = State::whereHas('subscriberAccess', function ($q) use($user) {
$q->whereHas('user', function ($q) use($user) {
$q->where('email', $user->email);
});
})->get()->pluck('name');
//Finding company state
$companyState = State::whereHas('companyOffice', function ($q) use($company) {
$q->whereHas('company', function ($q) use($company) {
$q->where('slug', $company->slug);
});
})->get()->pluck('name');
if($userState->intersect($companyState)->all())
return true;
else
return false;
}
}
And registered this to AuthServiceProvider
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
'Models\User' => 'Policies\CompanyPolicy',
];
While trying to fetch something like this in my controller:
public function companyGeneral(Request $request)
{
$user = Auth::user();
$company = Company::where('slug', $request->slug)
->with('offices')
->get()->first();
if($user->can('view', $company))
return response()->json(['data' => $company], 200);
else
return response()->json(['data' => 'Unauthorised'], 403);
}
Everytime I am getting Unauthorised response. Guide me into this. Thanks

Resources