advanced-nova-media-library icon indicating copy to clipboard operation
advanced-nova-media-library copied to clipboard

Filtering existing media by user_id

Open LiamKarlMitchell opened this issue 1 year ago • 0 comments

For adding user_id model relation to media library table Related: https://github.com/spatie/laravel-medialibrary/issues/75

For Laravel 9, I wanted to store authenticated user against the Media, but also customize the searching of existing media using Advanced Nova Media Library

Additionally A user should only be able to remove the media if they are Admin or Authenticated as the owner, but thats an implementation detail so I'll leave it out here. But to quote from this question

This package doesn't handle anything regarding permissions, that's something you should handle yourself in your app.

To go further on that I may need to make a Policy or Gate? for Media.

Model

sail artisan make:model Media
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\MediaLibrary\MediaCollections\Models\Media as BaseMedia;

class Media extends BaseMedia
{
    /**
     * All the relationships to be touched.
     *
     * @var array
     */
    protected $touches = ['model'];

    /**
     * Boot events
     * @return void
     */
    public static function boot()
    {
        parent::boot();

        static::creating(function ($media) {
            $user = auth()->getUser();
            if ($user) {
                $media->user_id = $user->id;
            }
        });
    }

    /**
     * User that uploaded the image.
     *
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Migration:

sail artisan make:migration add_user_to_media_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('media', function (Blueprint $table) {
            $table->foreignId('user_id')->index()->nullable()->constrained()->comment('The user that owns this record');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('media', function (Blueprint $table) {
            $table->dropConstrainedForeignId('user_id');
        });
    }
};
sail artisan migrate

Extending Advanced Nova Media Library Media Controller to query existing media based on User that uploaded the media.

Make a controller as needed.

App\Http\Controllers\AdvancedNovaMediaLibrary\MediaController.php

<?php

namespace App\Http\Controllers\AdvancedNovaMediaLibrary;

use App\Http\Controllers\Controller;
use Ebess\AdvancedNovaMediaLibrary\Http\Requests\MediaRequest;
use Ebess\AdvancedNovaMediaLibrary\Http\Resources\MediaResource;
use Exception;
use Auth;

class MediaController extends Controller
{
    public function index(MediaRequest $request)
    {
        if (!config('nova-media-library.enable-existing-media')) {
            throw new Exception('You need to enable the `existing media` feature via config.');
        }

        $hideCollections = config('nova-media-library.hide-media-collections', []);
        $mediaClass = config('media-library.media_model');
        $mediaClassIsSearchable = method_exists($mediaClass, 'search');

        $searchText = $request->input('search_text') ?: null;
        $perPage = $request->input('per_page') ?: 15;

        $query = null;

        if ($searchText && $mediaClassIsSearchable) {
            $query = $mediaClass::search($searchText);
        } else {
            $query = $mediaClass::query();

            if ($searchText) {
                $query->where(function ($query) use ($searchText) {
                    $query->where('name', 'LIKE', '%' . $searchText . '%');
                    $query->orWhere('file_name', 'LIKE', '%' . $searchText . '%');
                });
            }

            $query->latest();
        }

        if (!empty($hideCollections)) {
            if (!is_array($hideCollections)) {
                $hideCollections = [ $hideCollections ];
            }

            $query->whereNotIn('collection_name', $hideCollections);
        }

        // Custom code: Filter by uploaded user.
        $user = Auth::getUser(); //$request->getUser();
        if ($user) {
            $query->whereUserId($user->id);
        }
        // End of custom code.

        $results = $query->paginate($perPage);

        return MediaResource::collection($results);
    }
}

Make a route accordingly to your controller? Well this one was tricky as I could not seem to over-ride the routes made from vendor package due to load order like below.

~web.php~

Route::get('/media', [AdvancedNovaMediaLibrary\MediaController::class, 'index']);

But I could get composer to load my own file by overloading vendor classes. https://downing.tech/posts/overriding-vendor-classes

I created an Overrides/Ebess/AdvancedNovaMediaLibrary/AdvancedNovaMediaLibraryServiceProvider.php

<?php

namespace Ebess\AdvancedNovaMediaLibrary;

use Illuminate\Support\Facades\Route;
use Laravel\Nova\Nova;
use Laravel\Nova\Events\ServingNova;
use Illuminate\Support\ServiceProvider;

class AdvancedNovaMediaLibraryServiceProvider extends ServiceProvider
{
    /***
     * @var string Path to vendor files will be figured out in constructor.
     */
    private $vendorpath = '';

    /**
     * Create a new service provider instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function __construct($app)
    {
        $this->app = $app;
        $this->vendorpath = base_path('vendor/ebess/advanced-nova-media-library');
    }
    public function boot()
    {
        $this->publishes([
            $this->vendorpath . '/config/nova-media-library.php' => config_path('nova-media-library.php'),
        ], 'nova-media-library');

        $this->app->booted(function () {
            $this->routes();
        });

        Nova::serving(function (ServingNova $event) {
            Nova::script('media-lib-images-field', $this->vendorpath.'/dist/js/field.js');
        });
    }

    protected function routes()
    {
        if ($this->app->routesAreCached()) {
            return;
        }

        Route::middleware(['nova'])
            ->prefix('nova-vendor/ebess/advanced-nova-media-library')
            ->group(base_path('routes/novamedia.php'));
    }
}

Then I update my composer.json to not load the vendor file and load mine instead.

    "autoload": {
        "exclude-from-classmap": [
            "vendor/ebess/advanced-nova-media-library/src/AdvancedNovaMediaLibraryServiceProvider.php"
        ],
        "psr-4": {
            //...
            "Ebess\\AdvancedNovaMediaLibrary\\": "app/Overrides/Ebess/AdvancedNovaMediaLibrary"
        }
    },

Then I run

sail composer dump-autoload

Looking back I probably could have just done the same but for the Controller (not sure).

And of course update the model in config/media-library.php to point to your custom one.

    /*
     * The fully qualified class name of the media model.
     */
    'media_model' => Media::class

And with all that, I was able to have the Nova Media Library filter existing images by the user that uploaded the image. Would also be nice to filter by the collection or type of model the image is being uploaded for...

LiamKarlMitchell avatar Feb 04 '23 05:02 LiamKarlMitchell