voyager-admin/voyager

View on GitHub
src/Http/Controllers/BreadController.php

Summary

Maintainability
D
2 days
Test Coverage
F
57%
<?php

namespace Voyager\Admin\Http\Controllers;

use DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Voyager\Admin\Contracts\Formfields\Features;
use Voyager\Admin\Exceptions\NoLayoutFoundException;
use Voyager\Admin\Facades\Voyager as VoyagerFacade;
use Voyager\Admin\Manager\Breads as BreadManager;
use Voyager\Admin\Traits\Bread\Browsable;
use Voyager\Admin\Traits\Bread\Saveable;
use Voyager\Admin\Traits\Translatable;

class BreadController extends Controller
{
    use Browsable, Saveable;

    public bool $uses_soft_deletes = false;

    public function __construct(protected BreadManager $breadmanager)
    {
        parent::__construct();
    }

    public function browse(Request $request): InertiaResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'browse');

        return $this->inertiaRender('Bread/Browse', __('voyager::bread.browse_type', ['type' => $bread->name_plural]), [
            'bread'         => $bread,
            'relationships' => $bread->relationships,
            'defaultOrder'  => $layout->options->default_order_column->column ?? null,
        ]);
    }

    public function data(Request $request): array
    {
        $start = microtime(true);
        $bread = $this->getBread($request);
        $forcedLayout = $request->get('forcedLayout', null);
        if ($forcedLayout !== null) {
            $layout = $bread->layouts->where('uuid', $forcedLayout)->first();
            // TODO: Authorize the layout
        } else {
            $layout = $this->breadmanager->getLayoutForAction($bread, 'browse');
        }
        

        $warnings = [];

        $perpage = $request->get('perpage', 10);
        $global = $request->get('global', '');
        $filters = $request->get('filters', []);
        $filter = $request->get('filter', null);
        $softdeleted = $request->get('softdeleted', 'show');
        $locale = $request->get('locale', VoyagerFacade::getLocale());

        $query = $bread->getModel();

        if (!empty($layout->options->scope)) {
            $query = $query->{$layout->options->scope}();
        }

        // Apply custom scope
        $query = $this->applyCustomScope($bread, $layout, $filter, $query, $warnings);

        // Soft-deletes
        $query = $this->loadSoftDeletesQuery($bread, $layout, $softdeleted, $query);

        $total = $query->count();

        // Eager load relationships
        $query = $this->eagerLoadRelationships($layout, $query, $warnings);

        // Custom filter
        $query = $this->applyCustomFilter($bread, $layout, $filter, $query);

        // Global search ($global)
        $query = $this->globalSearchQuery($global, $layout, $locale, $query);

        // Column search ($filters)
        $query = $this->columnSearchQuery($filters, $layout, $query, $locale, $warnings);

        // Ordering ($order)
        $query = $this->orderQuery($layout, $request->get('direction', 'asc'), $request->get('order', null), $query, $locale, $warnings);

        $filtered = $total;
        if (!empty($global) || count($filters) > 0) {
            // TODO: We'd have to add an aggregate "count(*) as filtered" to remove one duplicate query
            $filtered = $query->count();
        }

        // Pagination ($perpage). At this point $query gets a Collection
        $query = $query->skip(($request->get('page', 1) - 1) * $perpage)->take($perpage)->get();

        // Load accessors
        $accessors = $layout->getFormfieldsByColumnType('computed')->pluck('column.column')->toArray();
        $query = $query->each(function ($item) use ($accessors) {
            $item->append($accessors);
        });

        // Transform results
        $query = $this->transformResults($layout, $bread->usesTranslatableTrait(), $query, $global, $filters);

        return [
            'results'           => $query->values(),
            'filtered'          => $filtered,
            'total'             => $total,
            'layout'            => $layout,
            'execution'         => number_format(((microtime(true) - $start) * 1000), 0, '.', ''),
            'uses_soft_deletes' => $this->uses_soft_deletes,
            'uses_ordering'     => ($bread->order_field !== null),
            'actions'           => $this->breadmanager->getActionsForBread($bread)->values(),
            'warnings'          => $warnings,
        ];
    }

    public function add(Request $request): array|InertiaResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'add');

        $new = true;
        $data = collect();

        $relationships = $bread->relationships->values();

        $layout->formfields->each(function ($formfield) use (&$data) {
            $data->put($formfield->column->column, $formfield->add());
        });

        if ($request->has('from_relationship')) {
            return compact('bread', 'layout', 'new', 'data', 'relationships');
        }

        return $this->inertiaRender('Bread/EditAdd', __('voyager::generic.add_type', ['type' => $bread->name_singular]), [
            'bread'         => $bread,
            'action'        => 'add',
            'layout'        => $layout,
            'relationships' => $relationships,
            'input'         => $data,
            'prev-url'      => url()->previous(),
            'primary-key'   => 0,
        ]);
    }

    public function store(Request $request): Response|JsonResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'add');

        $model = new $bread->model();
        $data = $request->get('data', []);

        if ($bread->usesTranslatableTrait()) {
            $model->dontTranslate(); // @phpstan-ignore-line
        }

        // Validate Data
        $validate_all_locales = $layout->options->validate_locales == 'all';
        $validation_errors = $this->validateData($layout->formfields, $data, $validate_all_locales);
        if (count($validation_errors) > 0) {
            return response()->json($validation_errors, 422);
        }

        $model = $this->updateStoreData($layout->formfields, $data, $model, false);

        if ($model->save()) {
            $layout->formfields->each(function ($formfield) use ($data, $model) {
                $formfield->stored($model, $data[$formfield->column->column]);
            });

            // Some formfields need to do something after the model was stored.
            // Relationships for example need to know the key of the created entry.
            $model->save();

            return response($model->getKey(), 200);
        } else {
            return response($model->getKey(), 500);
        }
    }

    public function read(Request $request, mixed $id): InertiaResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'read');

        $data = $bread->getModel()->findOrFail($id);
        if (!empty($layout->options->scope)) {
            $data = $bread->getModel()->{$layout->options->scope}()->findOrFail($id);
        }

        $layout->formfields->each(function ($formfield) use (&$data) {
            $value = $data->{$formfield->column->column};

            if ($formfield->translatable ?? false) {
                $translations = [];
                $value = json_decode($value) ?? [];
                foreach ($value as $locale => $translated) {
                    $translations[$locale] = $formfield->read($translated);
                }
                $data->{$formfield->column->column} = $translations;
            } else {
                $data->{$formfield->column->column} = $formfield->read($value);
            }
        });

        return $this->inertiaRender('Bread/Read', __('voyager::generic.show_type', ['type' => $bread->name_singular]), [
            'bread'         => $bread,
            'layout'        => $layout,
            'data'          => $data,
            'primary'       => $data->getKey(),
            'input'         => $data,
            'prev-url'      => url()->previous(),
            'relationships' => $bread->relationships->values(),
        ]);
    }

    public function edit(Request $request, mixed $id): InertiaResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'edit');

        $data = $bread->getModel()->findOrFail($id);
        $pk = $id;

        if (!empty($layout->options->scope)) {
            $data = $bread->getModel()->{$layout->options->scope}()->findOrFail($id);
        }

        if ($bread->usesTranslatableTrait()) {
            $data->dontTranslate();
        }

        $relationships = $bread->relationships->values();

        $data = (object) json_decode($data->toJson());
        $breadData = (object)[];

        $layout->formfields->each(function ($formfield) use ($data, &$breadData) {
            $value = $data->{$formfield->column->column} ?? null;

            if ($formfield->translatable ?? false) {
                $translations = [];
                $value = json_decode($value) ?? [];
                foreach ($value as $locale => $translated) {
                    $translations[$locale] = $formfield->edit($translated);
                }
                $breadData->{$formfield->column->column} = $translations;
            } else {
                $breadData->{$formfield->column->column} = $formfield->edit($value);
            }
        });

        return $this->inertiaRender('Bread/EditAdd', __('voyager::generic.edit_type', ['type' => $bread->name_singular]), [
            'bread'         => $bread,
            'action'        => 'edit',
            'layout'        => $layout,
            'relationships' => $relationships,
            'input'         => $breadData,
            'prev-url'      => url()->previous(),
            'primary-key'   => $pk,
        ]);
    }

    public function update(Request $request, mixed $id): Response|JsonResponse
    {
        $bread = $this->getBread($request, true);
        $layout = $this->breadmanager->getLayoutForAction($bread, 'edit');

        $model = $bread->getModel()->findOrFail($id);
        if (!empty($layout->options->scope)) {
            $model = $bread->getModel()->{$layout->options->scope}()->findOrFail($id);
        }
        $data = $request->get('data', []);

        if ($bread->usesTranslatableTrait()) {
            $model->dontTranslate();
        }

        // Validate Data
        $validate_all_locales = $layout->options->validate_locales == 'all';
        $validation_errors = $this->validateData($layout->formfields, $data, $validate_all_locales);
        if (count($validation_errors) > 0) {
            return response()->json($validation_errors, 422);
        }
        $model = $this->updateStoreData($layout->formfields, $data, $model);

        if ($model->save()) {
            $layout->formfields->each(function ($formfield) use ($data, $model) {
                $formfield->updated($model, $data[$formfield->column->column]);
            });

            return response($model->getKey(), 200);
        } else {
            return response($model->getKey(), 500);
        }
    }

    public function delete(Request $request): array
    {
        $bread = $this->getBread($request, true);
        $model = $bread->getModel();

        $deleted = 0;

        if ($request->has('primary')) {
            $ids = $request->get('primary');
            if (!is_array($ids)) {
                $ids = [$ids];
            }
            $model->find($ids)->each(function (Model $entry) use (&$deleted) {
                $this->authorize('delete', $entry);
                $entry->delete();
                $deleted++;
            });
        }

        return [
            'amount' => $deleted,
        ];
    }

    public function restore(Request $request): ?array
    {
        // TODO: Check if layout allows usage of soft-deletes
        $bread = $this->getBread($request);
        if (!$bread->usesSoftDeletes()) {
            return null;
        }

        $restored = 0;

        $model = $bread->getModel()->withTrashed(); // @phpstan-ignore-line

        if ($request->has('primary')) {
            $ids = $request->get('primary');
            if (!is_array($ids)) {
                $ids = [$ids];
            }

            $model->find($ids)->each(function ($entry) use (&$restored) {
                if ($entry->trashed()) {
                    $this->authorize('restore', $entry);
                    $entry->restore();
                    $restored++;
                }
            });
        }

        return [
            'amount' => $restored,
        ];
    }

    public function order(Request $request): Response
    {
        $key = $request->get('key', null);
        $up = $request->get('up', true);

        if (!is_null($key)) {
            $bread = $this->getBread($request);
            $model = $bread->getModel();

            $move_item = $model->findOrFail($key);
            $current_order = $move_item->{$bread->order_field};

            $next_item = $model->where($bread->order_field, ($up ? $current_order - 1 : $current_order + 1))->first();
            if ($next_item && $move_item instanceof Model) {
                $next_item->{$bread->order_field} = $current_order;
                $next_item->save();

                $move_item->{$bread->order_field} = ($up ? $current_order - 1 : $current_order + 1);
                $move_item->save();
            }
        }

        return response('', 200);
    }

    public function relationship(Request $request): array
    {
        // TODO: Validate that the method exists in edit/add layout
        $bread = $this->getBread($request, true);
        list($perPage, $query, $method, $column) = array_values($request->only(['perPage', 'query', 'method', 'column', 'key']));
        $translatable = $computed = false;

        if (Str::startsWith($column, 'computed.')) {
            $computed = true;
        }
        
        $relationship = $bread->relationships->where('method', $method)->first();

        if (!$relationship) {
            throw new \Exception('Relationship "'+$method+'" does not exist');
        }

        $data = $bread->getModel()->{$method}()->getRelated();
        $relatedTable = $data->getTable();
        $relatedKey = $data->getKeyName();

        $selected = collect();
        $model = $bread->getModel()->find($request->get('key', null));

        // Test if field is translatable
        $traits = class_uses($data);
        if ($traits !== false && in_array(Translatable::class, $traits) && in_array($column, $data->translatable)) {
            $translatable = true;
        }

        if ($computed) {
            list(, $accessor) = explode('.', $column);
            if ($model) {
                $selected = $model->{$method}->transform(function ($item) use ($column, $accessor) {
                    $item->{$column} = $item->{$accessor};

                    return $item;
                })->mapWithKeys(function ($item) use ($column, $relatedKey) {
                    return [$item->{$relatedKey} => $item->{$column}];
                });
            }

            $data = $data->get()->transform(function ($item) use ($column, $accessor) {
                $item->{$column} = $item->{$accessor};

                return $item;
            })->filter(function ($item) use ($query, $column) {
                if (!empty($query)) {
                    if (is_string($item->{$column})) {
                        return Str::contains(strtolower($item->{$column}), strtolower($query));
                    }
                }

                return true;
            })->sortBy(function ($item) use ($column) {
                return $item->{$column};
            });
        } else {
            if ($model) {
                $selected = $model->{$method}()->selectRaw("$relatedTable.$relatedKey, $relatedTable.$column")->pluck($column, $relatedKey);
            }

            if (!empty($query)) {
                if ($translatable) {
                    $data = $data->where(DB::raw('lower('.$column.'->"$.'.VoyagerFacade::getLocale().'")'), 'LIKE', '%'.$query.'%');
                } else {
                    $data = $data->where(DB::raw('lower('.$column.')'), 'LIKE', '%'.$query.'%');
                }
            }
        }

        $count = $data->count();

        if ($computed) {
            $data = $data->skip(($request->get('page', 1) - 1) * $perPage)->take($perPage);
        } else {
            $data = $data->orderBy($column)->skip(($request->get('page', 1) - 1) * $perPage)->take($perPage)->get();
        }

        $selected = $selected->transform(function ($value, $key) use ($translatable) {
            return [
                'key' => $key,
                'value' => $translatable ? VoyagerFacade::translate($value) : $value
            ];
        });

        $data->transform(function ($item) use ($column, $translatable) {
            return [
                'key'       => $item->getKey(),
                'value'     => $translatable ? VoyagerFacade::translate($item->{$column}) : $item->{$column},
            ];
        });

        return [
            'pages'     => ceil($count / $perPage),
            'data'      => $data->values(),
            'selected'  => $selected->values()
        ];
    }
}