2021-05-27 18:17:50 +02:00

567 lines
15 KiB
PHP

<?php
/**
* @package Grav\Framework\Flex
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use ArrayAccess;
use Exception;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Grav\Framework\Form\Traits\FormTrait;
use Grav\Framework\Route\Route;
use JsonSerializable;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
use RuntimeException;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper;
/**
* Class FlexForm
* @package Grav\Framework\Flex
*/
class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
use NestedArrayAccessWithGetters {
NestedArrayAccessWithGetters::get as private traitGet;
NestedArrayAccessWithGetters::set as private traitSet;
}
use FormTrait {
FormTrait::doSerialize as doTraitSerialize;
FormTrait::doUnserialize as doTraitUnserialize;
}
/** @var array */
private $items = [];
/** @var array|null */
private $form;
/** @var FlexObjectInterface */
private $object;
/** @var string */
private $flexName;
/** @var callable|null */
private $submitMethod;
/**
* @param array $options Options to initialize the form instance:
* (string) name: Form name, allows you to use custom form.
* (string) unique_id: Unique id for this form instance.
* (array) form: Custom form fields.
* (FlexObjectInterface) object: Object instance.
* (string) key: Object key, used only if object instance isn't given.
* (FlexDirectory) directory: Flex Directory, mandatory if object isn't given.
*
* @return FlexFormInterface
*/
public static function instance(array $options = [])
{
if (isset($options['object'])) {
$object = $options['object'];
if (!$object instanceof FlexObjectInterface) {
throw new RuntimeException(__METHOD__ . "(): 'object' should be instance of FlexObjectInterface", 400);
}
} elseif (isset($options['directory'])) {
$directory = $options['directory'];
if (!$directory instanceof FlexDirectory) {
throw new RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
}
$key = $options['key'] ?? '';
$object = $directory->getObject($key) ?? $directory->createObject([], $key);
} else {
throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory' or 'object'", 400);
}
$name = $options['name'] ?? '';
// There is no reason to pass object and directory.
unset($options['object'], $options['directory']);
return $object->getForm($name, $options);
}
/**
* FlexForm constructor.
* @param string $name
* @param FlexObjectInterface $object
* @param array|null $options
*/
public function __construct(string $name, FlexObjectInterface $object, array $options = null)
{
$this->name = $name;
$this->setObject($object);
$this->setName($object->getFlexType(), $name);
$this->setId($this->getName());
$uniqueId = $options['unique_id'] ?? null;
if (!$uniqueId) {
if ($object->exists()) {
$uniqueId = $object->getStorageKey();
} elseif ($object->hasKey()) {
$uniqueId = "{$object->getKey()}:new";
} else {
$uniqueId = "{$object->getFlexType()}:new";
}
$uniqueId = md5($uniqueId);
}
$this->setUniqueId($uniqueId);
$directory = $object->getFlexDirectory();
$this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
$this->form = $options['form'] ?? null;
if (!empty($options['reset'])) {
$this->getFlash()->delete();
}
$this->initialize();
}
/**
* @return $this
*/
public function initialize()
{
$this->messages = [];
$this->submitted = false;
$this->data = null;
$this->files = [];
$this->unsetFlash();
/** @var FlexFormFlash $flash */
$flash = $this->getFlash();
if ($flash->exists()) {
$data = $flash->getData();
if (null !== $data) {
$data = new Data($data, $this->getBlueprint());
$data->setKeepEmptyValues(true);
$data->setMissingValuesAsNull(true);
}
$object = $flash->getObject();
if (null === $object) {
throw new RuntimeException('Flash has no object');
}
$this->object = $object;
$this->data = $data;
$includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
$this->files = $flash->getFilesByFields($includeOriginal);
}
return $this;
}
/**
* @param string $name
* @param mixed $default
* @param string|null $separator
* @return mixed
*/
public function get($name, $default = null, $separator = null)
{
switch (strtolower($name)) {
case 'id':
case 'uniqueid':
case 'name':
case 'noncename':
case 'nonceaction':
case 'action':
case 'data':
case 'files':
case 'errors';
case 'fields':
case 'blueprint':
case 'page':
$method = 'get' . $name;
return $this->{$method}();
}
return $this->traitGet($name, $default, $separator);
}
/**
* @param string $name
* @param mixed $value
* @param string|null $separator
* @return FlexForm
*/
public function set($name, $value, $separator = null)
{
switch (strtolower($name)) {
case 'id':
case 'uniqueid':
$method = 'set' . $name;
return $this->{$method}();
}
return $this->traitSet($name, $value, $separator);
}
/**
* @return string
*/
public function getName(): string
{
return $this->flexName;
}
/**
* @param callable|null $submitMethod
*/
public function setSubmitMethod(?callable $submitMethod): void
{
$this->submitMethod = $submitMethod;
}
/**
* @param string $type
* @param string $name
*/
protected function setName(string $type, string $name): void
{
// Make sure that both type and name do not have dash (convert dashes to underscores).
$type = str_replace('-', '_', $type);
$name = str_replace('-', '_', $name);
$this->flexName = $name ? "flex-{$type}-{$name}" : "flex-{$type}";
}
/**
* @return Data|FlexObjectInterface|object
*/
public function getData()
{
return $this->data ?? $this->getObject();
}
/**
* Get a value from the form.
*
* Note: Used in form fields.
*
* @param string $name
* @return mixed
*/
public function getValue(string $name)
{
// Attempt to get value from the form data.
$value = $this->data ? $this->data[$name] : null;
// Return the form data or fall back to the object property.
return $value ?? $this->getObject()->getFormValue($name);
}
/**
* @param string $name
* @return array|mixed|null
*/
public function getDefaultValue(string $name)
{
return $this->object->getDefaultValue($name);
}
/**
* @return array
*/
public function getDefaultValues(): array
{
return $this->object->getDefaultValues();
}
/**
* @return string
*/
public function getFlexType(): string
{
return $this->object->getFlexType();
}
/**
* Get form flash object.
*
* @return FormFlashInterface|FlexFormFlash
*/
public function getFlash()
{
if (null === $this->flash) {
$grav = Grav::instance();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(),
'object' => $this->getObject()
];
$this->flash = new FlexFormFlash($config);
$this->flash
->setUrl($grav['uri']->url)
->setUser($grav['user'] ?? null);
}
return $this->flash;
}
/**
* @return FlexObjectInterface
*/
public function getObject(): FlexObjectInterface
{
return $this->object;
}
/**
* @return FlexObjectInterface
*/
public function updateObject(): FlexObjectInterface
{
$data = $this->data instanceof Data ? $this->data->toArray() : [];
$files = $this->files;
return $this->getObject()->update($data, $files);
}
/**
* @return Blueprint
*/
public function getBlueprint(): Blueprint
{
if (null === $this->blueprint) {
try {
$blueprint = $this->getObject()->getBlueprint($this->name);
if ($this->form) {
// We have field overrides available.
$blueprint->extend(['form' => $this->form], true);
$blueprint->init();
}
} catch (RuntimeException $e) {
if (!isset($this->form['fields'])) {
throw $e;
}
// Blueprint is not defined, but we have custom form fields available.
$blueprint = new Blueprint(null, ['form' => $this->form]);
$blueprint->load();
$blueprint->setScope('object');
$blueprint->init();
}
$this->blueprint = $blueprint;
}
return $this->blueprint;
}
/**
* @return Route|null
*/
public function getFileUploadAjaxRoute(): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
}
return $object->route('/edit.json/task:media.upload');
}
/**
* @param string $field
* @param string $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
}
return $object->route('/edit.json/task:media.delete');
}
/**
* @param array $params
* @param string|null $extension
* @return string
*/
public function getMediaTaskRoute(array $params = [], string $extension = null): string
{
$grav = Grav::instance();
/** @var Flex $flex */
$flex = $grav['flex_objects'];
if (method_exists($flex, 'adminRoute')) {
return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json');
}
return '';
}
/**
* @param string $name
* @return mixed|null
*/
public function __get($name)
{
$method = "get{$name}";
if (method_exists($this, $method)) {
return $this->{$method}();
}
$form = $this->getBlueprint()->form();
return $form[$name] ?? null;
}
/**
* @param string $name
* @param mixed $value
* @return void
*/
public function __set($name, $value)
{
$method = "set{$name}";
if (method_exists($this, $method)) {
$this->{$method}($value);
}
}
/**
* @param string $name
* @return bool
*/
public function __isset($name)
{
$method = "get{$name}";
if (method_exists($this, $method)) {
return true;
}
$form = $this->getBlueprint()->form();
return isset($form[$name]);
}
/**
* @param string $name
* @return void
*/
public function __unset($name)
{
}
/**
* @return array|bool
*/
protected function getUnserializeAllowedClasses()
{
return [FlexObject::class];
}
/**
* Note: this method clones the object.
*
* @param FlexObjectInterface $object
* @return $this
*/
protected function setObject(FlexObjectInterface $object): self
{
$this->object = clone $object;
return $this;
}
/**
* @param string $layout
* @return Template|TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
protected function getTemplate($layout)
{
$grav = Grav::instance();
/** @var Twig $twig */
$twig = $grav['twig'];
return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
"flex-objects/layouts/_default/form/{$layout}.html.twig",
"forms/{$layout}/form.html.twig",
'forms/default/form.html.twig'
]
);
}
/**
* @param array $data
* @param array $files
* @return void
* @throws Exception
*/
protected function doSubmit(array $data, array $files)
{
/** @var FlexObject $object */
$object = clone $this->getObject();
$method = $this->submitMethod;
if ($method) {
$method($data, $files, $object);
} else {
$object->update($data, $files);
$object->save();
}
$this->setObject($object);
$this->reset();
}
/**
* @return array
*/
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'object' => $this->object,
];
}
/**
* @param array $data
* @return void
*/
protected function doUnserialize(array $data): void
{
$this->doTraitUnserialize($data);
$this->object = $data['object'];
}
/**
* Filter validated data.
*
* @param ArrayAccess|Data|null $data
* @return void
*/
protected function filterData($data = null): void
{
if ($data instanceof Data) {
$data->filter(true, true);
}
}
}