3

I have a (relatively) basic need in Nova that I can't seem to figure out and I slowly start to feel that I'm approaching things the wrong way. So, I've got a User, Company, Device and Transfer models and respectively resources, everything pretty default regarding the resource setup. The schema is the following:

  • users: id, company_id
  • companies: id, type_id, name where type_id is pointing to one of three pre-populated types (manufacturer, dealer, client)
  • devices: id, imei
  • transfers: id, from_company_id, to_company_id, accepted_at
  • and Transfer is in a Many-to-Many with Device.

The idea behind the transfers being that Manufacturers transfer to Dealers, Dealers transfer to Clients, so it's really only a one-way thing.

Now the problem occurs at the following crucial point in the logic: In my Transfer resource pages, I want to show different fields depending on the type of the company the currently authenticated user belongs to. Basically, if the company is:

  • Manufacturer, then display a DEALER column populated with the transfers' toCompany relation;
  • Dealer, then display a CONTRAGENT column populated with the transfers' fromCompany or toCompany relations (depending on which mathces the current auth() company)
  • Client, then display a DEALER column populated with the transfers' fromCompany

All of the described logic works fine with the following code (App\Nova\Transfer.php as is) UNTIL I wanted to finally display the transfer's devices on the details page:

<?php

namespace App\Nova;

use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Http\Requests\NovaRequest;

class Transfer extends Resource
{
    public static $model = \App\Models\Transfer::class;

    public static $title = 'id';

    public static $search = [
        'id',
    ];
    
    public static $with = [
        'fromCompany',
        'toCompany'
    ];

    public function fields(Request $request)
    {
        $company = auth()->company();
        
        if($company->hasType('manufacturer'))
        {
            $contragentTitle = 'Dealer';
            $contragent = 'toCompany';
        } 
        else if($company->hasType('dealer'))
        {
            //\Debugbar::info($this); //showing empty resource when populating the devices
            $contragentTitle = 'Contragent';
            $contragent = $this->fromCompany->is($company) ? 'toCompany' : 'fromCompany'; //exception here, since the resource is empty and fromCompany is null
        } 
        else
        {
            $contragentTitle = 'Dealer';
            $contragent = 'fromCompany';
        }
        
        $contragentCompanyField = BelongsTo::make("$contragentTitle company", $contragent, Company::class);
        
        if($company->hasType('dealer'))
        {
            $contragentCompanyField->displayUsing(function ($contragentCompany) use ($contragent){
                return $contragentCompany->title() . " (".($contragent == 'toCompany' ? 'Outgoing' : "Incoming").')';
            });
        }
        
        return [
            ID::make(__('ID'), 'id')->sortable(),
            $contragentCompanyField,
            BelongsToMany::make('Devices') //problematic field, when removed, everything is fine...
        ];
    }
    
    public static function indexQuery(NovaRequest $request, $query)
    {
        if(auth()->check())
        {
            return $query->where(function($subQuery){
                return $subQuery->where('from_company_id', auth()->company()->id)->orWhere('to_company_id', auth()->company()->id);
            });
        }
    }

    public function cards(Request $request)
    {
        return [];
    }

    public function filters(Request $request)
    {
        return [];
    }

    public function lenses(Request $request)
    {
        return [];
    }

    //action is working fine (additional canRun added to avoid policy conflicts)
    public function actions(Request $request)
    {
        return [
            (new Actions\AcceptTransfer())->showOnTableRow()->canSee(function ($request) {
                if ($request instanceof \Laravel\Nova\Http\Requests\ActionRequest) {
                    return true;  
                }
                
                return $this->resource->exists
                        && $this->resource->toCompany->is(auth()->company())
                        && $this->resource->accepted_at === null;
                
            })->canRun(function ($request) {
                return true;
            })
        ];
    }
}

Now the strange thing that is happening is that the fields() method gets called multiple times on multiple ajax requests behind the scenes with Nova and when populating the devices relationship table, it gets called without a resource, although a call is never actually needed (as far as I can grasp the mechanics behind Nova) or at least when fetching relationships, you must still have the model information (at least the ID) somewhere to fetch by... So basically, if I'm a user of a dealer company, I can't see the devices that are being transferred (currently throwing a calling is() on null exception).

Now, this happens to be a big problem, since it hinders most of the stuff I need for my transfers, but also generally I don't like my approach so far, so... What would be the right way to achieve this multi-layer resource? Ideally I'd like to define three different transfer resource classes and somehow tell nova which one to use based on the user's company's type (since branching will most probably just grow more complex and therefore uglier as of the current aproach), but I can't figure out the way to do so.

I've also considered moving this entire logic to a separate Nova tool, but I really don't know much about them yet and whether that would be the right option... The only thing stopping me is that I still won't be able to elegantly solve the multi-layer problem and will have to write much of the otherwise useful Nova CRUD logic and views myself...

Any explanations (regarding the multiple calls of fields() and why resource is empty) or general structural recommendations to solve this case would be greatly appreciated! Many thanks in advance!

EDIT: I was able to circumvent the error by taking advantage of viaResourceId, so instaed of $this I ended up using:

$transfer = $this->id ? $this->resource : \App\Models\Transfer::find($request->viaResourceId);

but the messy code and the unneeded calls still remain an open question. Thanks again in advance!

D. Petrov
  • 1,147
  • 15
  • 27

1 Answers1

0

Here is an example of how I handled this:

public function fields(NovaRequest $request)
{
    /** @var \App\Models\User $user */
    $user = $this->id ? $this->resource : \App\Models\User::find($request->viaResourceId);
    
    if ($user && $user->whatEver()) {
        // display special fields in preview/detail view
        return [...];
    }
    
    // display for index and if no model is found
    return [...];
}
alexwenzel
  • 2,361
  • 2
  • 15
  • 18