Open davinyoeun opened 7 months ago
I can confirm this is indeed a problem. The field depending on another field will just disappear.
@davinyoeun As far as I can tell, the support for field dependency was added in times of Laravel 9 and Nova 3 where Nova 4 was just released. I've tested this feature with several early versions of Nova 4 and this feature just doesn't work.
The commit seems be mostly #347
Can anyone even confirm this worked with Laravel 10 / Nova 4? Or at all?
I seem to get an empty PATCH
response, can anyone confirm?
@toonvandenbos @voidgraphics I've been diving into this problem and in dire need to catch up some time I haven't spent with Laravel and Nova the last +/- 3 years.
To my understanding, Nova 4 uses an update-fields
PATCH request to get the new state of a field. The middleware intercepting requests using requestHasParsableFlexibleInputs
only acts on ['POST', 'PUT']
defined in ParsesFlexibleAttributes.
// InterceptFlexibleAttributes
public function handle(Request $request, Closure $next): Response
{
if (! $this->requestHasParsableFlexibleInputs($request)) {
return $next($request);
}
...
}
// ParsesFlexibleAttributes
protected function requestHasParsableFlexibleInputs(Request $request)
{
return in_array($request->method(), ['POST', 'PUT']) &&
is_string($request->input(FlexibleAttribute::REGISTER));
}
Now is_string($request->input(FlexibleAttribute::REGISTER)
checks if a specific attribute is present FlexibleAttribute::REGISTER
which again contains ___nova_flexible_content_fields
. But this attribute is never added to the PATCH
request by nova. This attribute is added by FormField.vue
when doing a fill.
So I'm just wondering, did this ever work? Did Nova 4 started splitting the requests using a PATCH request?
My final take on this matter, more or less a proof of concept.
The support for field dependency native to Nova isn't supported by the middleware InterceptFlexibleAttributes
. It doesn't take into account that a patch
request is send to only update the state of the dependent fields, therefor the response of /nova-api/{resource}/{resourceId}/update-fields
returns only one field because each dependent field sends out its own syncField
request. In its current state it simply returns an empty json response because the field requesting for its current state is never found due to the nature of how this package groups fields (nested) in form of layouts.
The support for dependent fields can only be achieved if the native update-fields
Nova offers is replaced with a patched one.
I've moved the constant FLEXIBLE_FIELD_OFFSET
from the trait to the class FlexibleAttribute
because constants in traits is supported from php >= 8.2
and onwards.
The following three files from this package needs to be modified:
Whitecube\NovaFlexibleContent\Http\ParsesFlexibleAttributes
Whitecube\NovaFlexibleContent\Http\Middleware\InterceptFlexibleAttributes
Whitecube\NovaFlexibleContent\Http\FlexibleAttribute
A new route file should be created in the project routes
directory
nova-api.php
A controller extending Novas UpdateFieldController
controller:
App\Http\Controllers\Nova\PatchedUpdateFieldController
A definition in App\Providers\RouteServiceProvider::boot
registering the routes/nova-api.php
routes file:
namespace App\Providers;
....
use Illuminate\Routing\Middleware\SubstituteBindings;
class RouteServiceProvider extends ServiceProvider
{
public function boot()
{
....
$this->routes(function () {
....
$this->novaApiRouteOverwrite();
});
....
}
/**
* Project based Nova API route file.
* @return void
*/
protected function novaApiRouteOverwrite() {
Route::group([
'domain' => config('nova.domain', null),
'as' => 'nova.api.',
'prefix' => 'nova-api',
'middleware' => 'nova:api',
'excluded_middleware' => [SubstituteBindings::class],
], function () {
$this->loadRoutesFrom(base_path('routes/nova-api.php'));
});
}
nova-api.php
route file:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Nova\PatchedUpdateFieldController;
Route::patch('/{resource}/{resourceId}/update-fields', [PatchedUpdateFieldController::class, 'sync']);
#### The `PatchedUpdatedFieldController`
- app/Http/Controllers/Nova/PatchedUpdatedFieldController.php
<?php
namespace App\Http\Controllers\Nova;
use Laravel\Nova\Http\Requests\ResourceUpdateOrUpdateAttachedRequest; use Laravel\Nova\Http\Resources\UpdateViewResource; use Laravel\Nova\Http\Controllers\UpdateFieldController; use Whitecube\NovaFlexibleContent\Flexible;
class PatchedUpdateFieldController extends UpdateFieldController {
/**
* @param ResourceUpdateOrUpdateAttachedRequest $request
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function sync(ResourceUpdateOrUpdateAttachedRequest $request)
{
$resource = UpdateViewResource::make()->newResourceWith($request);
return response()->json($resource->updateFields($request)
// we first need to extract all fields from a given group/layout
->map(function($field) use ($resource) {
if($field instanceof Flexible) {
$resolved = $field->jsonSerialize()['layouts']->map(function($layout) {
return $layout->fields();
})->flatten();
return $resolved;
}
return $field;
})
// we need to flatten the collection with all nested collections given previously
->flatten()
// here is everything as usual
->filter(function ($field) use ($request) {
return $request->query('field') === $field->attribute &&
$request->query('component') === $field->dependentComponentKey();
})->each->syncDependsOn($request)->first());
}
}
#### Modifications to the `InterceptFlexibleAttributes` middleware class, concerning only the `handle` method.
- file: src/Http/Middleware/
public function handle(Request $request, Closure $next): Response
{
// has been adapted to also parse patch requests of `update-fields`
if (! $this->requestHasParsableFlexibleInputs($request)) {
return $next($request);
}
// we assume we can quite here because we don't need the rest after if statement
if($request->method() === 'PATCH') {
return $next($request);
}
$request->merge($this->getParsedFlexibleInputs($request));
$request->request->remove(FlexibleAttribute::REGISTER);
$response = $next($request);
if (! $this->shouldTransformFlexibleErrors($response)) {
return $response;
}
return $this->transformFlexibleErrors($response);
}
#### Modifications to the `FlexibleAttribute` class:
<?php
namespace Whitecube\NovaFlexibleContent\Http;
use Illuminate\Support\Arr;
class FlexibleAttribute { .... /**
ParsesFlexibleAttributes
trait:<?php
namespace Whitecube\NovaFlexibleContent\Http;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
trait ParsesFlexibleAttributes
{
/**
* Offset of a generated flexible attribute where the group
* separator (FlexibleAttribute::GROUP_SEPARATOR) starts
* @note: supported from php >= 8.2
*/
// const FLEXIBLE_FIELD_OFFSET = 15;
/**
* @param $attribute
* @return void
*/
protected function splitFlexPartsFromFieldName(string $attribute) {
return array_combine(['key', 'field'], explode('__', $attribute));
}
/**
* Modify the request for a PATCH update-fields request
* @param $request
* @return bool
*/
protected function parseFlexableFieldForPatchRequest($request) : bool {
$field = $request->input('field');
// we firstly check if the group separator starts at char index 15 (16th char)
if(!(strlen($field) >= FlexibleAttribute::FLEXIBLE_FIELD_OFFSET &&
strpos($field, FlexibleAttribute::GROUP_SEPARATOR, FlexibleAttribute::FLEXIBLE_FIELD_OFFSET) !== false)) {
return false;
}
// flexible keys converted to original and to be merged with the request
$flex_fields = [];
$parts = $this->splitFlexPartsFromFieldName($field);
// here we overwrite the query parameter 'field'.
$request->instance()->query->set('field', $parts['field']);
foreach($request->all() as $field => $value) {
// check if we a have a flexible generated field name
if(Str::startsWith($field, $parts['key'])) {
// remove flexible generate input
$request->request->remove($field);
// pretend it's an original field input
$flex_fields[
str_replace($parts['key'] . FlexibleAttribute::GROUP_SEPARATOR, '', $field)
] = $value;
}
}
$request->merge($flex_fields);
return true;
}
/**
* Check if given request should be handled by the middleware
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function requestHasParsableFlexibleInputs(Request $request)
{
if($request->method() === 'PATCH' && Str::contains($request->getRequestUri(), '/update-fields?')) {
return $this->parseFlexableFieldForPatchRequest($request);
}
return in_array($request->method(), ['POST', 'PUT']) &&
is_string($request->input(FlexibleAttribute::REGISTER));
}
}
I'm not quite familiar with this package so some parts can certainly be done better. This part is quite ugly $resolved = $field->jsonSerialize()['layouts']->map
but I didn't find a better solution where a flexible field could resolve its groups and return a collection of fields. Maybe I just missed it.
Anyhow, it works as it should and I'll update this issue along with any future progress.
I have 2 fields below: