model = $model; $this->builder = new Builder($this); if ($callback instanceof Closure) { $callback($this); } $this->isSoftDeletes = in_array(SoftDeletes::class, class_uses($this->model)); if (static::$initCallback instanceof Closure) { call_user_func(static::$initCallback, $this); } } /** * Initialize with user pre-defined default disables, etc. * * @param Closure $callback */ public static function init(Closure $callback = null) { static::$initCallback = $callback; } /** * @param Field $field * * @return $this */ public function pushField(Field $field) { $field->setForm($this); $this->builder->fields()->push($field); return $this; } /** * @return Model */ public function model() { return $this->model; } /** * @return Builder */ public function builder() { return $this->builder; } /** * Generate a edit form. * * @param $id * * @return $this */ public function edit($id) { $this->builder->setMode(Builder::MODE_EDIT); $this->builder->setResourceId($id); $this->setFieldValue($id); return $this; } /** * Use tab to split form. * * @param string $title * @param Closure $content * * @return $this */ public function tab($title, Closure $content, $active = false) { $this->getTab()->append($title, $content, $active); return $this; } /** * Get Tab instance. * * @return Tab */ public function getTab() { if (is_null($this->tab)) { $this->tab = new Tab($this); } return $this->tab; } /** * Destroy data entity and remove files. * * @param $id * * @return mixed */ public function destroy($id) { collect(explode(',', $id))->filter()->each(function ($id) { $builder = $this->model()->newQuery(); if ($this->isSoftDeletes) { $builder = $builder->withTrashed(); } $model = $builder->with($this->getRelations())->findOrFail($id); if ($this->isSoftDeletes && $model->trashed()) { $this->deleteFiles($model, true); $model->forceDelete(); return; } $this->deleteFiles($model); $model->delete(); }); return true; } /** * Remove files in record. * * @param Model $model * @param bool $forceDelete */ protected function deleteFiles(Model $model, $forceDelete = false) { // If it's a soft delete, the files in the data will not be deleted. if (!$forceDelete && $this->isSoftDeletes) { return; } $data = $model->toArray(); $this->builder->fields()->filter(function ($field) { return $field instanceof Field\File; })->each(function (Field\File $file) use ($data) { $file->setOriginal($data); $file->destroy(); }); } /** * Store a new record. * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\Http\JsonResponse */ public function store() { $data = Input::all(); // Handle validation errors. if ($validationMessages = $this->validationMessages($data)) { return back()->withInput()->withErrors($validationMessages); } if (($response = $this->prepare($data)) instanceof Response) { return $response; } DB::transaction(function () { $inserts = $this->prepareInsert($this->updates); foreach ($inserts as $column => $value) { $this->model->setAttribute($column, $value); } $this->model->save(); $this->updateRelation($this->relations); }); if (($response = $this->callSaved()) instanceof Response) { return $response; } if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) { return $response; } return $this->redirectAfterStore(); } /** * Get ajax response. * * @param string $message * * @return bool|\Illuminate\Http\JsonResponse */ protected function ajaxResponse($message) { $request = Request::capture(); // ajax but not pjax if ($request->ajax() && !$request->pjax()) { return response()->json([ 'status' => true, 'message' => $message, ]); } return false; } /** * Prepare input data for insert or update. * * @param array $data * * @return mixed */ protected function prepare($data = []) { if (($response = $this->callSubmitted()) instanceof Response) { return $response; } $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs); if (($response = $this->callSaving()) instanceof Response) { return $response; } $this->relations = $this->getRelationInputs($this->inputs); $this->updates = array_except($this->inputs, array_keys($this->relations)); } /** * Remove ignored fields from input. * * @param array $input * * @return array */ protected function removeIgnoredFields($input) { array_forget($input, $this->ignored); return $input; } /** * Get inputs for relations. * * @param array $inputs * * @return array */ protected function getRelationInputs($inputs = []) { $relations = []; foreach ($inputs as $column => $value) { if (method_exists($this->model, $column)) { $relation = call_user_func([$this->model, $column]); if ($relation instanceof Relations\Relation) { $relations[$column] = $value; } } } return $relations; } /** * Call editing callbacks. * * @return void */ protected function callEditing() { foreach ($this->editing as $func) { call_user_func($func, $this); } } /** * Call submitted callback. * * @return mixed */ protected function callSubmitted() { foreach ($this->submitted as $func) { if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { return $ret; } } } /** * Call saving callback. * * @return mixed */ protected function callSaving() { foreach ($this->saving as $func) { if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { return $ret; } } } /** * Callback after saving a Model. * * @return mixed|null */ protected function callSaved() { foreach ($this->saved as $func) { if ($func instanceof Closure && ($ret = call_user_func($func, $this)) instanceof Response) { return $ret; } } } /** * Handle update. * * @param int $id * * @return \Symfony\Component\HttpFoundation\Response */ public function update($id, $data = null) { $data = ($data) ?: Input::all(); $isEditable = $this->isEditable($data); $data = $this->handleEditable($data); $data = $this->handleFileDelete($data); if ($this->handleOrderable($id, $data)) { return response([ 'status' => true, 'message' => trans('admin.update_succeeded'), ]); } /* @var Model $this->model */ $builder = $this->model(); if ($this->isSoftDeletes) { $builder = $builder->withTrashed(); } $this->model = $builder->with($this->getRelations())->findOrFail($id); $this->setFieldOriginalValue(); // Handle validation errors. if ($validationMessages = $this->validationMessages($data)) { if (!$isEditable) { return back()->withInput()->withErrors($validationMessages); } else { return response()->json(['errors' => array_dot($validationMessages->getMessages())], 422); } } if (($response = $this->prepare($data)) instanceof Response) { return $response; } DB::transaction(function () { $updates = $this->prepareUpdate($this->updates); foreach ($updates as $column => $value) { /* @var Model $this->model */ $this->model->setAttribute($column, $value); } $this->model->save(); $this->updateRelation($this->relations); }); if (($result = $this->callSaved()) instanceof Response) { return $result; } if ($response = $this->ajaxResponse(trans('admin.update_succeeded'))) { return $response; } return $this->redirectAfterUpdate($id); } /** * Get RedirectResponse after store. * * @return \Illuminate\Http\RedirectResponse */ protected function redirectAfterStore() { $resourcesPath = $this->resource(0); $key = $this->model->getKey(); return $this->redirectAfterSaving($resourcesPath, $key); } /** * Get RedirectResponse after update. * * @param mixed $key * * @return \Illuminate\Http\RedirectResponse */ protected function redirectAfterUpdate($key) { $resourcesPath = $this->resource(-1); return $this->redirectAfterSaving($resourcesPath, $key); } /** * Get RedirectResponse after data saving. * * @param string $resourcesPath * @param string $key * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ protected function redirectAfterSaving($resourcesPath, $key) { if (request('after-save') == 1) { // continue editing $url = rtrim($resourcesPath, '/')."/{$key}/edit"; } elseif (request('after-save') == 2) { // continue creating $url = rtrim($resourcesPath, '/').'/create'; } elseif (request('after-save') == 3) { // view resource $url = rtrim($resourcesPath, '/')."/{$key}"; } else { $url = request(Builder::PREVIOUS_URL_KEY) ?: $resourcesPath; } admin_toastr(trans('admin.save_succeeded')); return redirect($url); } /** * Check if request is from editable. * * @param array $input * * @return bool */ protected function isEditable(array $input = []) { return array_key_exists('_editable', $input); } /** * Handle editable update. * * @param array $input * * @return array */ protected function handleEditable(array $input = []) { if (array_key_exists('_editable', $input)) { $name = $input['name']; $value = $input['value']; array_forget($input, ['pk', 'value', 'name']); array_set($input, $name, $value); } return $input; } /** * @param array $input * * @return array */ protected function handleFileDelete(array $input = []) { if (array_key_exists(Field::FILE_DELETE_FLAG, $input)) { $input[Field::FILE_DELETE_FLAG] = $input['key']; unset($input['key']); } Input::replace($input); return $input; } /** * Handle orderable update. * * @param int $id * @param array $input * * @return bool */ protected function handleOrderable($id, array $input = []) { if (array_key_exists('_orderable', $input)) { $model = $this->model->find($id); if ($model instanceof Sortable) { $input['_orderable'] == 1 ? $model->moveOrderUp() : $model->moveOrderDown(); return true; } } return false; } /** * Update relation data. * * @param array $relationsData * * @return void */ protected function updateRelation($relationsData) { foreach ($relationsData as $name => $values) { if (!method_exists($this->model, $name)) { continue; } $relation = $this->model->$name(); $oneToOneRelation = $relation instanceof Relations\HasOne || $relation instanceof Relations\MorphOne || $relation instanceof Relations\BelongsTo; $prepared = $this->prepareUpdate([$name => $values], $oneToOneRelation); if (empty($prepared)) { continue; } switch (true) { case $relation instanceof Relations\BelongsToMany: case $relation instanceof Relations\MorphToMany: if (isset($prepared[$name])) { $relation->sync($prepared[$name]); } break; case $relation instanceof Relations\HasOne: $related = $this->model->$name; // if related is empty if (is_null($related)) { $related = $relation->getRelated(); $qualifiedParentKeyName = $relation->getQualifiedParentKeyName(); $localKey = array_last(explode('.', $qualifiedParentKeyName)); $related->{$relation->getForeignKeyName()} = $this->model->{$localKey}; } foreach ($prepared[$name] as $column => $value) { $related->setAttribute($column, $value); } $related->save(); break; case $relation instanceof Relations\BelongsTo: $parent = $this->model->$name; // if related is empty if (is_null($parent)) { $parent = $relation->getRelated(); } foreach ($prepared[$name] as $column => $value) { $parent->setAttribute($column, $value); } $parent->save(); // When in creating, associate two models if (!$this->model->{$relation->getForeignKey()}) { $this->model->{$relation->getForeignKey()} = $parent->getKey(); $this->model->save(); } break; case $relation instanceof Relations\MorphOne: $related = $this->model->$name; if (is_null($related)) { $related = $relation->make(); } foreach ($prepared[$name] as $column => $value) { $related->setAttribute($column, $value); } $related->save(); break; case $relation instanceof Relations\HasMany: case $relation instanceof Relations\MorphMany: foreach ($prepared[$name] as $related) { /** @var Relations\Relation $relation */ $relation = $this->model()->$name(); $keyName = $relation->getRelated()->getKeyName(); $instance = $relation->findOrNew(array_get($related, $keyName)); if ($related[static::REMOVE_FLAG_NAME] == 1) { $instance->delete(); continue; } array_forget($related, static::REMOVE_FLAG_NAME); $instance->fill($related); $instance->save(); } break; } } } /** * Prepare input data for update. * * @param array $updates * @param bool $oneToOneRelation If column is one-to-one relation. * * @return array */ protected function prepareUpdate(array $updates, $oneToOneRelation = false) { $prepared = []; /** @var Field $field */ foreach ($this->builder->fields() as $field) { $columns = $field->column(); // If column not in input array data, then continue. if (!array_has($updates, $columns)) { continue; } if ($this->invalidColumn($columns, $oneToOneRelation)) { continue; } $value = $this->getDataByColumn($updates, $columns); $value = $field->prepare($value); if (is_array($columns)) { foreach ($columns as $name => $column) { array_set($prepared, $column, $value[$name]); } } elseif (is_string($columns)) { array_set($prepared, $columns, $value); } } return $prepared; } /** * @param string|array $columns * @param bool $oneToOneRelation * * @return bool */ protected function invalidColumn($columns, $oneToOneRelation = false) { foreach ((array) $columns as $column) { if ((!$oneToOneRelation && Str::contains($column, '.')) || ($oneToOneRelation && !Str::contains($column, '.'))) { return true; } } return false; } /** * Prepare input data for insert. * * @param $inserts * * @return array */ protected function prepareInsert($inserts) { if ($this->isHasOneRelation($inserts)) { $inserts = array_dot($inserts); } foreach ($inserts as $column => $value) { if (is_null($field = $this->getFieldByColumn($column))) { unset($inserts[$column]); continue; } $inserts[$column] = $field->prepare($value); } $prepared = []; foreach ($inserts as $key => $value) { array_set($prepared, $key, $value); } return $prepared; } /** * Is input data is has-one relation. * * @param array $inserts * * @return bool */ protected function isHasOneRelation($inserts) { $first = current($inserts); if (!is_array($first)) { return false; } if (is_array(current($first))) { return false; } return Arr::isAssoc($first); } /** * Set after getting editing model callback. * * @param Closure $callback * * @return void */ public function editing(Closure $callback) { $this->editing[] = $callback; } /** * Set submitted callback. * * @param Closure $callback * * @return void */ public function submitted(Closure $callback) { $this->submitted[] = $callback; } /** * Set saving callback. * * @param Closure $callback * * @return void */ public function saving(Closure $callback) { $this->saving[] = $callback; } /** * Set saved callback. * * @param Closure $callback * * @return void */ public function saved(Closure $callback) { $this->saved[] = $callback; } /** * Ignore fields to save. * * @param string|array $fields * * @return $this */ public function ignore($fields) { $this->ignored = array_merge($this->ignored, (array) $fields); return $this; } /** * @param array $data * @param string|array $columns * * @return array|mixed */ protected function getDataByColumn($data, $columns) { if (is_string($columns)) { return array_get($data, $columns); } if (is_array($columns)) { $value = []; foreach ($columns as $name => $column) { if (!array_has($data, $column)) { continue; } $value[$name] = array_get($data, $column); } return $value; } } /** * Find field object by column. * * @param $column * * @return mixed */ protected function getFieldByColumn($column) { return $this->builder->fields()->first( function (Field $field) use ($column) { if (is_array($field->column())) { return in_array($column, $field->column()); } return $field->column() == $column; } ); } /** * Set original data for each field. * * @return void */ protected function setFieldOriginalValue() { // static::doNotSnakeAttributes($this->model); $values = $this->model->toArray(); $this->builder->fields()->each(function (Field $field) use ($values) { $field->setOriginal($values); }); } /** * Set all fields value in form. * * @param $id * * @return void */ protected function setFieldValue($id) { $relations = $this->getRelations(); $builder = $this->model(); if ($this->isSoftDeletes) { $builder = $builder->withTrashed(); } $this->model = $builder->with($relations)->findOrFail($id); $this->callEditing(); // static::doNotSnakeAttributes($this->model); $data = $this->model->toArray(); $this->builder->fields()->each(function (Field $field) use ($data) { if (!in_array($field->column(), $this->ignored)) { $field->fill($data); } }); } /** * Don't snake case attributes. * * @param Model $model * * @return void */ protected static function doNotSnakeAttributes(Model $model) { $class = get_class($model); $class::$snakeAttributes = false; } /** * Get validation messages. * * @param array $input * * @return MessageBag|bool */ public function validationMessages($input) { $failedValidators = []; /** @var Field $field */ foreach ($this->builder->fields() as $field) { if (!$validator = $field->getValidator($input)) { continue; } if (($validator instanceof Validator) && !$validator->passes()) { $failedValidators[] = $validator; } } $message = $this->mergeValidationMessages($failedValidators); return $message->any() ? $message : false; } /** * Merge validation messages from input validators. * * @param \Illuminate\Validation\Validator[] $validators * * @return MessageBag */ protected function mergeValidationMessages($validators) { $messageBag = new MessageBag(); foreach ($validators as $validator) { $messageBag = $messageBag->merge($validator->messages()); } return $messageBag; } /** * Get all relations of model from callable. * * @return array */ public function getRelations() { $relations = $columns = []; /** @var Field $field */ foreach ($this->builder->fields() as $field) { $columns[] = $field->column(); } foreach (array_flatten($columns) as $column) { if (str_contains($column, '.')) { list($relation) = explode('.', $column); if (method_exists($this->model, $relation) && $this->model->$relation() instanceof Relations\Relation ) { $relations[] = $relation; } } elseif (method_exists($this->model, $column) && !method_exists(Model::class, $column) ) { $relations[] = $column; } } return array_unique($relations); } /** * Set action for form. * * @param string $action * * @return $this */ public function setAction($action) { $this->builder()->setAction($action); return $this; } /** * Set field and label width in current form. * * @param int $fieldWidth * @param int $labelWidth * * @return $this */ public function setWidth($fieldWidth = 8, $labelWidth = 2) { $this->builder()->fields()->each(function ($field) use ($fieldWidth, $labelWidth) { /* @var Field $field */ $field->setWidth($fieldWidth, $labelWidth); }); $this->builder()->setWidth($fieldWidth, $labelWidth); return $this; } /** * Set view for form. * * @param string $view * * @return $this */ public function setView($view) { $this->builder()->setView($view); return $this; } /** * Set title for form. * * @param string $title * * @return $this */ public function setTitle($title = '') { $this->builder()->setTitle($title); return $this; } /** * Add a row in form. * * @param Closure $callback * * @return $this */ public function row(Closure $callback) { $this->rows[] = new Row($callback, $this); return $this; } /** * Tools setting for form. * * @param Closure $callback */ public function tools(Closure $callback) { $callback->call($this, $this->builder->getTools()); } /** * Disable form submit. * * @return $this * * @deprecated */ public function disableSubmit(bool $disable = true) { $this->builder()->getFooter()->disableSubmit($disable); return $this; } /** * Disable form reset. * * @return $this * * @deprecated */ public function disableReset(bool $disable = true) { $this->builder()->getFooter()->disableReset($disable); return $this; } /** * Disable View Checkbox on footer. * * @return $this */ public function disableViewCheck(bool $disable = true) { $this->builder()->getFooter()->disableViewCheck($disable); return $this; } /** * Disable Editing Checkbox on footer. * * @return $this */ public function disableEditingCheck(bool $disable = true) { $this->builder()->getFooter()->disableEditingCheck($disable); return $this; } /** * Disable Creating Checkbox on footer. * * @return $this */ public function disableCreatingCheck(bool $disable = true) { $this->builder()->getFooter()->disableCreatingCheck($disable); return $this; } /** * Footer setting for form. * * @param Closure $callback */ public function footer(Closure $callback) { call_user_func($callback, $this->builder()->getFooter()); } /** * Get current resource route url. * * @param int $slice * * @return string */ public function resource($slice = -2) { $segments = explode('/', trim(app('request')->getUri(), '/')); if ($slice != 0) { $segments = array_slice($segments, 0, $slice); } return implode('/', $segments); } /** * Render the form contents. * * @return string */ public function render() { try { return $this->builder->render(); } catch (\Exception $e) { return Handler::renderException($e); } } /** * Get or set input data. * * @param string $key * @param null $value * * @return array|mixed */ public function input($key, $value = null) { if (is_null($value)) { return array_get($this->inputs, $key); } return array_set($this->inputs, $key, $value); } /** * Register builtin fields. * * @return void */ public static function registerBuiltinFields() { $map = [ 'button' => Field\Button::class, 'checkbox' => Field\Checkbox::class, 'color' => Field\Color::class, 'currency' => Field\Currency::class, 'date' => Field\Date::class, 'dateRange' => Field\DateRange::class, 'datetime' => Field\Datetime::class, 'dateTimeRange' => Field\DatetimeRange::class, 'datetimeRange' => Field\DatetimeRange::class, 'decimal' => Field\Decimal::class, 'display' => Field\Display::class, 'divider' => Field\Divide::class, 'divide' => Field\Divide::class, 'embeds' => Field\Embeds::class, 'editor' => Field\Editor::class, 'email' => Field\Email::class, 'file' => Field\File::class, 'hasMany' => Field\HasMany::class, 'hidden' => Field\Hidden::class, 'id' => Field\Id::class, 'image' => Field\Image::class, 'ip' => Field\Ip::class, 'map' => Field\Map::class, 'mobile' => Field\Mobile::class, 'month' => Field\Month::class, 'multipleSelect' => Field\MultipleSelect::class, 'number' => Field\Number::class, 'password' => Field\Password::class, 'radio' => Field\Radio::class, 'rate' => Field\Rate::class, 'select' => Field\Select::class, 'slider' => Field\Slider::class, 'switch' => Field\SwitchField::class, 'text' => Field\Text::class, 'textarea' => Field\Textarea::class, 'time' => Field\Time::class, 'timeRange' => Field\TimeRange::class, 'url' => Field\Url::class, 'year' => Field\Year::class, 'html' => Field\Html::class, 'tags' => Field\Tags::class, 'icon' => Field\Icon::class, 'multipleFile' => Field\MultipleFile::class, 'multipleImage' => Field\MultipleImage::class, 'captcha' => Field\Captcha::class, 'listbox' => Field\Listbox::class, ]; foreach ($map as $abstract => $class) { static::extend($abstract, $class); } } /** * Register custom field. * * @param string $abstract * @param string $class * * @return void */ public static function extend($abstract, $class) { static::$availableFields[$abstract] = $class; } /** * Set form field alias. * * @param string $field * @param string $alias * * @return void */ public static function alias($field, $alias) { static::$fieldAlias[$alias] = $field; } /** * Remove registered field. * * @param array|string $abstract */ public static function forget($abstract) { array_forget(static::$availableFields, $abstract); } /** * Find field class. * * @param string $method * * @return bool|mixed */ public static function findFieldClass($method) { // If alias exists. if (isset(static::$fieldAlias[$method])) { $method = static::$fieldAlias[$method]; } $class = array_get(static::$availableFields, $method); if (class_exists($class)) { return $class; } return false; } /** * Collect assets required by registered field. * * @return array */ public static function collectFieldAssets() { if (!empty(static::$collectedAssets)) { return static::$collectedAssets; } $css = collect(); $js = collect(); foreach (static::$availableFields as $field) { if (!method_exists($field, 'getAssets')) { continue; } $assets = call_user_func([$field, 'getAssets']); $css->push(array_get($assets, 'css')); $js->push(array_get($assets, 'js')); } return static::$collectedAssets = [ 'css' => $css->flatten()->unique()->filter()->toArray(), 'js' => $js->flatten()->unique()->filter()->toArray(), ]; } /** * Getter. * * @param string $name * * @return array|mixed */ public function __get($name) { return $this->input($name); } /** * Setter. * * @param string $name * @param $value */ public function __set($name, $value) { return array_set($this->inputs, $name, $value); } /** * Generate a Field object and add to form builder if Field exists. * * @param string $method * @param array $arguments * * @return Field */ public function __call($method, $arguments) { if ($className = static::findFieldClass($method)) { $column = array_get($arguments, 0, ''); //[0]; $element = new $className($column, array_slice($arguments, 1)); $this->pushField($element); return $element; } admin_error('Error', "Field type [$method] does not exist."); return new Field\Nullable(); } }