Added classes for defining relationships

This commit is contained in:
Matias Griese
2022-04-28 13:55:25 +03:00
parent 6218a4b366
commit 6ba1cff114
20 changed files with 1851 additions and 55 deletions

View File

@@ -12,7 +12,7 @@
"homepage": "https://getgrav.org",
"license": "MIT",
"require": {
"php": "^7.3.6 || ^8.0",
"php": "^7.4.1 || ^8.0",
"ext-json": "*",
"ext-openssl": "*",
"ext-curl": "*",
@@ -89,7 +89,7 @@
"config": {
"apcu-autoloader": true,
"platform": {
"php": "7.3.6"
"php": "7.4.1"
}
},
"autoload": {

110
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f0530b0fd3e574fef0852376653da5a0",
"content-hash": "7645535ce7348e38bad6dad9ce8b2fdb",
"packages": [
{
"name": "composer/ca-bundle",
@@ -1655,20 +1655,20 @@
},
{
"name": "psr/container",
"version": "1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
"php": ">=7.4.0"
},
"type": "library",
"autoload": {
@@ -1697,9 +1697,9 @@
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
"source": "https://github.com/php-fig/container/tree/1.1.2"
},
"time": "2021-03-05T17:36:06+00:00"
"time": "2021-11-05T16:50:12+00:00"
},
{
"name": "psr/http-factory",
@@ -2224,16 +2224,16 @@
},
{
"name": "symfony/console",
"version": "v4.4.40",
"version": "v4.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "bdcc66f3140421038f495e5b50e3ca6ffa14c773"
"reference": "0e1e62083b20ccb39c2431293de060f756af905c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/bdcc66f3140421038f495e5b50e3ca6ffa14c773",
"reference": "bdcc66f3140421038f495e5b50e3ca6ffa14c773",
"url": "https://api.github.com/repos/symfony/console/zipball/0e1e62083b20ccb39c2431293de060f756af905c",
"reference": "0e1e62083b20ccb39c2431293de060f756af905c",
"shasum": ""
},
"require": {
@@ -2294,7 +2294,7 @@
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/console/tree/v4.4.40"
"source": "https://github.com/symfony/console/tree/v4.4.41"
},
"funding": [
{
@@ -2310,7 +2310,7 @@
"type": "tidelift"
}
],
"time": "2022-03-26T22:12:04+00:00"
"time": "2022-04-12T15:19:55+00:00"
},
{
"name": "symfony/contracts",
@@ -2492,16 +2492,16 @@
},
{
"name": "symfony/http-client",
"version": "v4.4.40",
"version": "v4.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "c66fc3b60900359ea10a7b22921c797446783bb3"
"reference": "bad7c3296590c5a69a9ed89e8a51f13c07c34b54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/c66fc3b60900359ea10a7b22921c797446783bb3",
"reference": "c66fc3b60900359ea10a7b22921c797446783bb3",
"url": "https://api.github.com/repos/symfony/http-client/zipball/bad7c3296590c5a69a9ed89e8a51f13c07c34b54",
"reference": "bad7c3296590c5a69a9ed89e8a51f13c07c34b54",
"shasum": ""
},
"require": {
@@ -2553,7 +2553,7 @@
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-client/tree/v4.4.40"
"source": "https://github.com/symfony/http-client/tree/v4.4.41"
},
"funding": [
{
@@ -2569,7 +2569,7 @@
"type": "tidelift"
}
],
"time": "2022-04-01T12:25:39+00:00"
"time": "2022-04-12T15:19:55+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -3063,16 +3063,16 @@
},
{
"name": "symfony/process",
"version": "v4.4.40",
"version": "v4.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "54e9d763759268e07eb13b921d8631fc2816206f"
"reference": "9eedd60225506d56e42210a70c21bb80ca8456ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/54e9d763759268e07eb13b921d8631fc2816206f",
"reference": "54e9d763759268e07eb13b921d8631fc2816206f",
"url": "https://api.github.com/repos/symfony/process/zipball/9eedd60225506d56e42210a70c21bb80ca8456ce",
"reference": "9eedd60225506d56e42210a70c21bb80ca8456ce",
"shasum": ""
},
"require": {
@@ -3105,7 +3105,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v4.4.40"
"source": "https://github.com/symfony/process/tree/v4.4.41"
},
"funding": [
{
@@ -3121,20 +3121,20 @@
"type": "tidelift"
}
],
"time": "2022-03-18T16:18:39+00:00"
"time": "2022-04-04T10:19:07+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v4.4.39",
"version": "v4.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "35237c5e5dcb6593a46a860ba5b29c1d4683d80e"
"reference": "58eb36075c04aaf92a7a9f38ee9a8b97e24eb481"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/35237c5e5dcb6593a46a860ba5b29c1d4683d80e",
"reference": "35237c5e5dcb6593a46a860ba5b29c1d4683d80e",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/58eb36075c04aaf92a7a9f38ee9a8b97e24eb481",
"reference": "58eb36075c04aaf92a7a9f38ee9a8b97e24eb481",
"shasum": ""
},
"require": {
@@ -3194,7 +3194,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v4.4.39"
"source": "https://github.com/symfony/var-dumper/tree/v4.4.41"
},
"funding": [
{
@@ -3210,7 +3210,7 @@
"type": "tidelift"
}
],
"time": "2022-02-25T10:38:15+00:00"
"time": "2022-04-25T21:15:06+00:00"
},
{
"name": "symfony/yaml",
@@ -3862,20 +3862,24 @@
},
{
"name": "codeception/stub",
"version": "3.7.0",
"version": "4.0.2",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Stub.git",
"reference": "468dd5fe659f131fc997f5196aad87512f9b1304"
"reference": "18a148dacd293fc7b044042f5aa63a82b08bff5d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codeception/Stub/zipball/468dd5fe659f131fc997f5196aad87512f9b1304",
"reference": "468dd5fe659f131fc997f5196aad87512f9b1304",
"url": "https://api.github.com/repos/Codeception/Stub/zipball/18a148dacd293fc7b044042f5aa63a82b08bff5d",
"reference": "18a148dacd293fc7b044042f5aa63a82b08bff5d",
"shasum": ""
},
"require": {
"phpunit/phpunit": "^8.4 | ^9.0"
"php": "^7.4 | ^8.0",
"phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | 10.0.x-dev"
},
"require-dev": {
"consolidation/robo": "^3.0"
},
"type": "library",
"autoload": {
@@ -3890,9 +3894,9 @@
"description": "Flexible Stub wrapper for PHPUnit's Mock Builder",
"support": {
"issues": "https://github.com/Codeception/Stub/issues",
"source": "https://github.com/Codeception/Stub/tree/3.7.0"
"source": "https://github.com/Codeception/Stub/tree/4.0.2"
},
"time": "2020-07-03T15:54:43+00:00"
"time": "2022-01-31T19:25:15+00:00"
},
{
"name": "doctrine/instantiator",
@@ -4679,16 +4683,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.6.0",
"version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "b480ba2ae0699a72d43a340c20b9c00ede91ee3e"
"reference": "becb9603a31d70f5007d505877a7b812598dfe46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b480ba2ae0699a72d43a340c20b9c00ede91ee3e",
"reference": "b480ba2ae0699a72d43a340c20b9c00ede91ee3e",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/becb9603a31d70f5007d505877a7b812598dfe46",
"reference": "becb9603a31d70f5007d505877a7b812598dfe46",
"shasum": ""
},
"require": {
@@ -4714,7 +4718,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.6.0"
"source": "https://github.com/phpstan/phpstan/tree/1.6.2"
},
"funding": [
{
@@ -4734,7 +4738,7 @@
"type": "tidelift"
}
],
"time": "2022-04-26T05:43:03+00:00"
"time": "2022-04-27T11:05:24+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -6505,16 +6509,16 @@
},
{
"name": "symfony/finder",
"version": "v5.4.3",
"version": "v5.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d"
"reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d",
"reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d",
"url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9",
"reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9",
"shasum": ""
},
"require": {
@@ -6548,7 +6552,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v5.4.3"
"source": "https://github.com/symfony/finder/tree/v5.4.8"
},
"funding": [
{
@@ -6564,7 +6568,7 @@
"type": "tidelift"
}
],
"time": "2022-01-26T16:34:36+00:00"
"time": "2022-04-15T08:07:45+00:00"
},
{
"name": "theseer/tokenizer",
@@ -6681,7 +6685,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.3.6 || ^8.0",
"php": "^7.4.1 || ^8.0",
"ext-json": "*",
"ext-openssl": "*",
"ext-curl": "*",
@@ -6691,7 +6695,7 @@
},
"platform-dev": [],
"platform-overrides": {
"php": "7.3.6"
"php": "7.4.1"
},
"plugin-api-version": "2.2.0"
}

View File

@@ -50,6 +50,13 @@ interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObj
*/
public function metadata();
/**
* Returns an array containing the file metadata
*
* @return array
*/
public function getMeta();
/**
* Add meta file for the medium.
*

View File

@@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Media;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Media Object Interface
*/
interface MediaObjectInterface extends IdentifierInterface
{
/**
* Returns true if the object exists.
*
* @return bool
* @phpstan-pure
*/
public function exists(): bool;
/**
* Get metadata associated to the media object.
*
* @return array
* @phpstan-pure
*/
public function getMeta(): array;
/**
* @param string $field
* @return mixed
* @phpstan-pure
*/
public function get(string $field);
/**
* Return URL pointing to the media object.
*
* @return string
* @phpstan-pure
*/
public function getUrl(): string;
/**
* Create media response.
*
* @param array $actions
* @return ResponseInterface
* @phpstan-pure
*/
public function createResponse(array $actions): ResponseInterface;
}

View File

@@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Object;
use JsonSerializable;
/**
* Interface IdentifierInterface
*/
interface IdentifierInterface extends JsonSerializable
{
/**
* Get identifier's ID.
*
* @return string
* @phpstan-pure
*/
public function getId(): string;
/**
* Get identifier's type.
*
* @return string
* @phpstan-pure
*/
public function getType(): string;
}

View File

@@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface RelationshipIdentifierInterface
*/
interface RelationshipIdentifierInterface extends IdentifierInterface
{
/**
* If identifier has meta.
*
* @return bool
* @phpstan-pure
*/
public function hasIdentifierMeta(): bool;
/**
* Get identifier meta.
*
* @return array|ArrayAccess
* @phpstan-pure
*/
public function getIdentifierMeta();
}

View File

@@ -0,0 +1,80 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Countable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use IteratorAggregate;
use JsonSerializable;
use Serializable;
/**
* Interface Relationship
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
*/
interface RelationshipInterface extends Countable, IteratorAggregate, JsonSerializable, Serializable
{
/**
* @return string
* @phpstan-pure
*/
public function getName(): string;
/**
* @return string
* @phpstan-pure
*/
public function getType(): string;
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string;
/**
* @return P
* @phpstan-pure
*/
public function getParent(): IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool;
/**
* @param T $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool;
/**
* @param T|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool;
/**
* @return iterable<T>
*/
public function getIterator(): iterable;
}

View File

@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use ArrayAccess;
use Countable;
use Iterator;
use JsonSerializable;
/**
* Interface RelationshipsInterface
*/
interface RelationshipsInterface extends Countable, ArrayAccess, Iterator, JsonSerializable
{
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool;
/**
* @return array
*/
public function getModified(): array;
/**
* @return int
* @phpstan-pure
*/
public function count(): int;
/**
* @param string $offset
* @return RelationshipInterface|null
*/
public function offsetGet($offset): ?RelationshipInterface;
/**
* @return RelationshipInterface|null
*/
public function current(): ?RelationshipInterface;
/**
* @return string
* @phpstan-pure
*/
public function key(): string;
}

View File

@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToManyRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToManyRelationshipInterface extends RelationshipInterface
{
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface;
/**
* @param string $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id, string $type = null): ?object;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool;
/**
* @param iterable<T> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool;
}

View File

@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Contracts\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface ToOneRelationshipInterface
*
* @template T of IdentifierInterface
* @template P of IdentifierInterface
* @template-extends RelationshipInterface<T,P>
*/
interface ToOneRelationshipInterface extends RelationshipInterface
{
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface;
/**
* @param string|null $id
* @param string|null $type
* @return T|null
* @phpstan-pure
*/
public function getObject(string $id = null, string $type = null): ?object;
/**
* @param T|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool;
}

View File

@@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex;
use Grav\Common\Grav;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
use RuntimeException;
/**
* Interface IdentifierInterface
*
* @template T of FlexObjectInterface
*/
class FlexIdentifier extends Identifier
{
private string $keyField;
private ?FlexObjectInterface $object = null;
/**
* @param FlexObjectInterface $object
* @return FlexIdentifier
*/
public static function createFromObject(FlexObjectInterface $object): FlexIdentifier
{
$instance = new static($object->getKey(), $object->getFlexType(), 'key');
$instance->setObject($object);
return $instance;
}
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
* @param string $keyField
*/
public function __construct(string $id, string $type, string $keyField = 'key')
{
parent::__construct($id, $type);
$this->keyField = $keyField;
}
/**
* @return T
*/
public function getObject(): ?FlexObjectInterface
{
if (!isset($this->object)) {
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
$this->object = $flex->getObject($this->getId(), $this->getType(), $this->keyField);
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(FlexObjectInterface $object): void
{
$type = $this->getType();
if ($type !== $object->getFlexType()) {
throw new RuntimeException(sprintf('Object has to be type %s, %s given', $type, $object->getFlexType()));
}
$this->object = $object;
}
}

View File

@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Flex\Traits;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Relationships\Relationships;
trait FlexRelationshipsTrait
{
private ?RelationshipsInterface $_relationships = null;
/**
* @return Relationships
*/
public function getRelationships(): Relationships
{
if (!isset($this->_relationships)) {
$blueprint = $this->getBlueprint();
$options = $blueprint->get('config/relationships', []);
$parent = FlexIdentifier::createFromObject($this);
$this->_relationships = new Relationships($parent, $options);
}
return $this->_relationships;
}
/**
* @param string $name
* @return RelationshipInterface|null
*/
public function getRelationship(string $name): ?RelationshipInterface
{
return $this->getRelationships()[$name];
}
protected function resetRelationships(): void
{
$this->_relationships = null;
}
}

View File

@@ -0,0 +1,135 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\Identifiers\Identifier;
/**
* Interface IdentifierInterface
*
* @template T of MediaObjectInterface
*/
class MediaIdentifier extends Identifier
{
private ?MediaObjectInterface $object = null;
/**
* @param MediaObjectInterface $object
* @return MediaIdentifier
*/
public static function createFromObject(MediaObjectInterface $object): MediaIdentifier
{
$instance = new static($object->getId());
$instance->setObject($object);
return $instance;
}
/**
* @param string $id
*/
public function __construct(string $id)
{
parent::__construct($id, 'media');
}
/**
* @return T
*/
public function getObject(): ?MediaObjectInterface
{
if (!isset($this->object)) {
$type = $this->getType();
$id = $this->getId();
$parts = explode('/', $id);
if ($type === 'media' && str_starts_with($id, 'uploads/')) {
array_shift($parts);
$type = array_shift($parts);
$uniqueId = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
$flash = $this->getFlash($type, $uniqueId);
if ($flash->exists()) {
$uploadedFile = $flash->getFilesByField($field)[$filename] ?? null;
$this->object = new UploadedMediaObject($field, $filename, $flash, $uploadedFile);
}
} else {
$type = array_shift($parts);
$key = array_shift($parts);
$field = array_shift($parts);
$filename = implode('/', $parts);
$flexObject = $this->getFlexObject($type, $key);
if ($flexObject && method_exists($flexObject, 'getMediaField') && method_exists($flexObject, 'getMedia')) {
$media = $field !== 'media' ? $flexObject->getMediaField($field) : $flexObject->getMedia();
$image = null;
if ($media) {
$image = $media[$filename];
}
$this->object = new MediaObject($field, $filename, $image, $flexObject);
}
}
if (!isset($this->object)) {
throw new \RuntimeException(sprintf('Object not found for identifier {type: "%s", id: "%s"}', $type, $id));
}
}
return $this->object;
}
/**
* @param T $object
*/
public function setObject(MediaObjectInterface $object): void
{
$type = $this->getType();
$objectType = $object->getType();
if ($type !== $objectType) {
throw new \RuntimeException(sprintf('Object has to be type %s, %s given', $type, $objectType));
}
$this->object = $object;
}
protected function getFlash(string $type, string $uniqueId): FlexFormFlash
{
/** @var UserInterface|null $user */
$user = Grav::instance()['user'] ?? null;
if (null !== $user && $user->exists()) {
// TODO: Modify uniqueid so we can detect if flash is user or session based.
$mediaFolder = $user->getMediaFolder();
} else {
// TODO: Implement session based flash.
throw new \RuntimeException('Not implemented');
}
$folder = "{$mediaFolder}/tmp/api/flex-{$type}";
$config = [
'unique_id' => $uniqueId,
'folder' => $folder
];
return new FlexFormFlash($config);
}
protected function getFlexObject(string $type, string $key): ?FlexObjectInterface
{
/** @var Flex $flex */
$flex = Grav::instance()['flex'];
return $flex->getObject($key, $type);
}
}

View File

@@ -0,0 +1,210 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Common\Media\Interfaces\MediaObjectInterface as GravMediaObjectInterface;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Psr7\Response;
use Throwable;
/**
* Class MediaObject
*/
class MediaObject implements MediaObjectInterface
{
// FIXME:
static public string $placeholderImage = 'theme://img/revkit-temp.svg';
public FlexObjectInterface $object;
public ?GravMediaObjectInterface $media;
private ?string $field;
private string $filename;
/**
* MediaObject constructor.
* @param string|null $field
* @param string $filename
* @param GravMediaObjectInterface|null $media
* @param FlexObjectInterface $object
*/
public function __construct(?string $field, string $filename, ?GravMediaObjectInterface $media, FlexObjectInterface $object)
{
$this->field = $field;
$this->filename = $filename;
$this->media = $media;
$this->object = $object;
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$field = $this->field;
$object = $this->object;
$path = $field ? "/{$field}/" : '/media/';
return $object->getType() . '/' . $object->getKey() . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
return $this->media !== null;
}
/**
* @return array
*/
public function getMeta(): array
{
if (!isset($this->media)) {
return [];
}
return $this->media->getMeta();
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
if (!isset($this->media)) {
return null;
}
return $this->media->get($field);
}
/**
* @return string
*/
public function getUrl(): string
{
if (!isset($this->media)) {
return '';
}
return $this->media->url();
}
/**
* Create media response.
*
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): Response
{
if (!isset($this->media)) {
return $this->create404Response($actions);
}
$media = $this->media;
if ($actions) {
$media = $this->processMediaActions($media, $actions);
}
// FIXME: This only works for images
if (!$media instanceof ImageMedium) {
throw new \RuntimeException('Not Implemented', 500);
}
$filename = $media->path(false);
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => $media->get('mime'),
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(200, $headers, $body);
}
/**
* Process media actions
*
* @param GravMediaObjectInterface $medium
* @param array $actions
* @return GravMediaObjectInterface
*/
protected function processMediaActions(GravMediaObjectInterface $medium, array $actions): GravMediaObjectInterface
{
// loop through actions for the image and call them
foreach ($actions as $method => $params) {
$matches = [];
if (preg_match('/\[(.*)]/', $params, $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $params);
}
try {
$medium->{$method}(...$args);
} catch (Throwable $e) {
// Ignore all errors for now and just skip the action.
}
}
return $medium;
}
/**
* @param array $actions
* @return Response
*/
protected function create404Response(array $actions): Response
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@@ -0,0 +1,158 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Media;
use Grav\Framework\Contracts\Media\MediaObjectInterface;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Grav\Framework\Psr7\Response;
use Psr\Http\Message\UploadedFileInterface;
/**
* Class UploadedMediaObject
*/
class UploadedMediaObject implements MediaObjectInterface
{
// FIXME:
static public string $placeholderImage = 'theme://img/revkit-temp.svg';
public FormFlashInterface $object;
private ?string $field;
private string $filename;
private array $meta;
private ?UploadedFileInterface $uploadedFile;
/**
* UploadedMediaObject constructor.
* @param string|null $field
* @param string $filename
* @param FormFlashInterface $object
* @param UploadedFileInterface|null $uploadedFile
*/
public function __construct(?string $field, string $filename, FormFlashInterface $object, ?UploadedFileInterface $uploadedFile = null)
{
$this->field = $field;
$this->filename = $filename;
$this->object = $object;
$this->uploadedFile = $uploadedFile;
if ($uploadedFile) {
$this->meta = [
'filename' => $uploadedFile->getClientFilename(),
'mime' => $uploadedFile->getClientMediaType(),
'size' => $uploadedFile->getSize()
];
} else {
$this->meta = [];
}
}
/**
* @return string
*/
public function getType(): string
{
return 'media';
}
/**
* @return string
*/
public function getId(): string
{
$field = $this->field;
$object = $this->object;
if ($object instanceof FlexFormFlash) {
$type = $object->getObject()->getFlexType();
} else {
$type = 'undefined';
}
$id = $type . '/' . $object->getUniqueId();
$path = $field ? "/{$field}/" : '';
return 'uploads/' . $id . $path . basename($this->filename);
}
/**
* @return bool
*/
public function exists(): bool
{
//return $this->uploadedFile !== null;
return false;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
/**
* @param string $field
* @return mixed|null
*/
public function get(string $field)
{
return $this->meta[$field] ?? null;
}
/**
* @return string
*/
public function getUrl(): string
{
return '';
}
/**
* @return UploadedFileInterface|null
*/
public function getUploadedFile(): ?UploadedFileInterface
{
return $this->uploadedFile;
}
/**
* @param array $actions
* @return Response
*/
public function createResponse(array $actions): Response
{
// Display placeholder image.
$filename = static::$placeholderImage;
$time = filemtime($filename);
$size = filesize($filename);
$body = fopen($filename, 'rb');
$headers = [
'Content-Type' => 'image/svg',
'Last-Modified' => gmdate('D, d M Y H:i:s', $time) . ' GMT',
'ETag' => sprintf('%x-%x', $size, $time)
];
return new Response(404, $headers, $body);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'id' => $this->getId()
];
}
/**
* @return string[]
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Object\Identifiers;
use Grav\Framework\Contracts\Object\IdentifierInterface;
/**
* Interface IdentifierInterface
*
* @template T of object
*/
class Identifier implements IdentifierInterface
{
private string $id;
private string $type;
/**
* IdentifierInterface constructor.
* @param string $id
* @param string $type
*/
public function __construct(string $id, string $type)
{
$this->id = $id;
$this->type = $type;
}
/**
* @return string
* @phpstan-pure
*/
public function getId(): string
{
return $this->id;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'type' => $this->type,
'id' => $this->id
];
}
/**
* @return array
*/
public function __debugInfo(): array
{
return $this->jsonSerialize();
}
}

View File

@@ -0,0 +1,211 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\RelationshipInterface;
use Grav\Framework\Contracts\Relationships\RelationshipsInterface;
use Grav\Framework\Flex\FlexIdentifier;
use RuntimeException;
use function count;
/**
* Class Relationships
*/
class Relationships implements RelationshipsInterface
{
protected IdentifierInterface $parent;
protected array $options;
/** @var RelationshipInterface[] */
protected array $relationships;
/**
* Relationships constructor.
* @param IdentifierInterface $parent
* @param array $options
*/
public function __construct(IdentifierInterface $parent, array $options)
{
$this->parent = $parent;
$this->options = $options;
$this->relationships = [];
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return !empty($this->getModified());
}
/**
* @return RelationshipInterface[]
* @phpstan-pure
*/
public function getModified(): array
{
$list = [];
foreach ($this->relationships as $name => $relationship) {
if ($relationship->isModified()) {
$list[$name] = $relationship;
}
}
return $list;
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->options);
}
/**
* @param string $offset
* @return bool
* @phpstan-pure
*/
public function offsetExists($offset): bool
{
return isset($this->options[$offset]);
}
/**
* @param string $offset
* @return RelationshipInterface|null
*/
public function offsetGet($offset): ?RelationshipInterface
{
if (!isset($this->relationships[$offset])) {
$options = $this->options[$offset] ?? null;
if (null === $options) {
return null;
}
$this->relationships[$offset] = $this->createRelationship($offset, $options);
}
return $this->relationships[$offset];
}
/**
* @param string $offset
* @param mixed $value
* @return never-return
*/
public function offsetSet($offset, $value)
{
throw new RuntimeException('Setting relationship is not supported', 500);
}
/**
* @param string $offset
* @return never-return
*/
public function offsetUnset($offset)
{
throw new RuntimeException('Removing relationship is not allowed', 500);
}
/**
* @return RelationshipInterface|null
*/
public function current(): ?RelationshipInterface
{
$name = key($this->options);
if ($name === null) {
return null;
}
return $this->offsetGet($name);
}
/**
* @return string
* @phpstan-pure
*/
public function key(): string
{
return key($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function next(): void
{
next($this->options);
}
/**
* @return void
* @phpstan-pure
*/
public function rewind(): void
{
reset($this->options);
}
/**
* @return bool
* @phpstan-pure
*/
public function valid(): bool
{
return key($this->options) !== null;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this as $name => $relationship) {
$list[$name] = $relationship->jsonSerialize();
}
return $list;
}
/**
* @param string $name
* @param array $options
* @return RelationshipInterface
*/
private function createRelationship(string $name, array $options): RelationshipInterface
{
$data = null;
$parent = $this->parent;
if ($parent instanceof FlexIdentifier) {
$object = $parent->getObject();
if (!method_exists($object, 'initRelationship')) {
throw new RuntimeException(sprintf('Bad relationship %s', $name), 500);
}
$data = $object->initRelationship($name);
}
$cardinality = $options['cardinality'] ?? '';
switch ($cardinality) {
case 'to-one':
$relationship = new ToOneRelationship($parent, $name, $options, $data);
break;
case 'to-many':
$relationship = new ToManyRelationship($parent, $name, $options, $data ?? []);
break;
default:
throw new RuntimeException(sprintf('Bad relationship cardinality %s', $cardinality), 500);
}
return $relationship;
}
}

View File

@@ -0,0 +1,244 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToManyRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function count;
use function is_callable;
/**
* Class ToManyRelationship
*
* @template T of object
* @template P of object
* @template-implements ToManyRelationshipInterface<IdentifierInterface,IdentifierInterface>
*/
class ToManyRelationship implements ToManyRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
/** @var IdentifierInterface[] */
protected array $identifiers = [];
/**
* ToManyRelationship constructor.
* @param string $name
* @param IdentifierInterface $parent
* @param iterable<IdentifierInterface> $identifiers
*/
public function __construct(IdentifierInterface $parent, string $name, array $options, iterable $identifiers = [])
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->addIdentifiers($identifiers);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-many';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return count($this->identifiers);
}
/**
* @return array
*/
public function fetch(): array
{
$list = [];
foreach ($this->identifiers as $identifier) {
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
$list[] = $identifier;
}
return $list;
}
/**
* @param string $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param string $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id, string $type = null): ?IdentifierInterface
{
if (null === $type) {
$type = $this->getType();
}
if ($type === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$key = "{$type}/{$id}";
return $this->identifiers[$key] ?? null;
}
/**
* @param string $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
return $this->addIdentifiers([$identifier]);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
return !$identifier || $this->removeIdentifiers([$identifier]);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function addIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
$this->identifiers[$key] = $this->checkIdentifier($identifier);
$this->modified = true;
}
return true;
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function replaceIdentifiers(iterable $identifiers): bool
{
$this->identifiers = [];
$this->modified = true;
return $this->addIdentifiers($identifiers);
}
/**
* @param iterable<IdentifierInterface> $identifiers
* @return bool
*/
public function removeIdentifiers(iterable $identifiers): bool
{
foreach ($identifiers as $identifier) {
$type = $identifier->getType();
$id = $identifier->getId();
$key = "{$type}/{$id}";
unset($this->identifiers[$key]);
$this->modified = true;
}
return true;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator($this->identifiers);
}
/**
* @return array
*/
public function jsonSerialize(): array
{
$list = [];
foreach ($this->getIterator() as $item) {
$list[] = $item->jsonSerialize();
}
return $list;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifiers' => $this->identifiers,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifiers = $data['identifiers'];
}
}

View File

@@ -0,0 +1,205 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships;
use ArrayIterator;
use Grav\Framework\Compat\Serializable;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Contracts\Relationships\ToOneRelationshipInterface;
use Grav\Framework\Relationships\Traits\RelationshipTrait;
use function is_callable;
/**
* Class ToOneRelationship
*
* @template T of object
* @template-implements ToOneRelationshipInterface<IdentifierInterface,IdentifierInterface>
*/
class ToOneRelationship implements ToOneRelationshipInterface
{
/** @template-use RelationshipTrait<T> */
use RelationshipTrait;
use Serializable;
protected ?IdentifierInterface $identifier = null;
public function __construct(IdentifierInterface $parent, string $name, array $options, IdentifierInterface $identifier = null)
{
$this->parent = $parent;
$this->name = $name;
$this->parseOptions($options);
$this->replaceIdentifier($identifier);
$this->modified = false;
}
/**
* @return string
* @phpstan-pure
*/
public function getCardinality(): string
{
return 'to-one';
}
/**
* @return int
* @phpstan-pure
*/
public function count(): int
{
return $this->identifier ? 1 : 0;
}
/**
* @return object|null
*/
public function fetch(): ?object
{
$identifier = $this->identifier;
if (is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return bool
* @phpstan-pure
*/
public function has(string $id = null, string $type = null): bool
{
return $this->getIdentifier($id, $type) !== null;
}
/**
* @param string|null $id
* @param string|null $type
* @return IdentifierInterface|null
* @phpstan-pure
*/
public function getIdentifier(string $id = null, string $type = null): ?IdentifierInterface
{
if ($id && $this->getType() === 'media' && !str_contains($id, '/')) {
$name = $this->name;
$id = $this->parent->getType() . '/' . $this->parent->getId() . '/'. $name . '/' . $id;
}
$identifier = $this->identifier ?? null;
if (null === $identifier || ($type && $type !== $identifier->getType()) || ($id && $id !== $identifier->getId())) {
return null;
}
return $identifier;
}
/**
* @param string|null $id
* @param string|null $type
* @return T|null
*/
public function getObject(string $id = null, string $type = null): ?object
{
$identifier = $this->getIdentifier($id, $type);
if ($identifier && is_callable([$identifier, 'getObject'])) {
$identifier = $identifier->getObject();
}
return $identifier;
}
/**
* @param IdentifierInterface $identifier
* @return bool
*/
public function addIdentifier(IdentifierInterface $identifier): bool
{
$this->identifier = $this->checkIdentifier($identifier);
$this->modified = true;
return true;
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function replaceIdentifier(IdentifierInterface $identifier = null): bool
{
if ($identifier === null) {
$this->identifier = null;
$this->modified = true;
return true;
}
return $this->addIdentifier($identifier);
}
/**
* @param IdentifierInterface|null $identifier
* @return bool
*/
public function removeIdentifier(IdentifierInterface $identifier = null): bool
{
if (null === $identifier || $this->has($identifier->getId(), $identifier->getType())) {
$this->identifier = null;
$this->modified = true;
return true;
}
return false;
}
/**
* @return iterable<IdentifierInterface>
* @phpstan-pure
*/
public function getIterator(): iterable
{
return new ArrayIterator((array)$this->identifier);
}
/**
* @return array|null
*/
public function jsonSerialize(): ?array
{
return $this->identifier ? $this->identifier->jsonSerialize() : null;
}
/**
* @return array
*/
public function __serialize(): array
{
return [
'parent' => $this->parent,
'name' => $this->name,
'type' => $this->type,
'options' => $this->options,
'modified' => $this->modified,
'identifier' => $this->identifier,
];
}
/**
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->parent = $data['parent'];
$this->name = $data['name'];
$this->type = $data['type'];
$this->options = $data['options'];
$this->modified = $data['modified'];
$this->identifier = $data['identifier'];
}
}

View File

@@ -0,0 +1,122 @@
<?php declare(strict_types=1);
namespace Grav\Framework\Relationships\Traits;
use Grav\Framework\Contracts\Object\IdentifierInterface;
use Grav\Framework\Flex\FlexIdentifier;
use Grav\Framework\Media\MediaIdentifier;
use Grav\Framework\Object\Identifiers\Identifier;
use function get_class;
/**
* Trait RelationshipTrait
*
* @template T of object
*/
trait RelationshipTrait
{
protected IdentifierInterface $parent;
protected string $name;
protected string $type;
protected array $options;
protected bool $modified = false;
/**
* @return string
* @phpstan-pure
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
* @phpstan-pure
*/
public function getType(): string
{
return $this->type;
}
/**
* @return bool
* @phpstan-pure
*/
public function isModified(): bool
{
return $this->modified;
}
/**
* @return IdentifierInterface
* @phpstan-pure
*/
public function getParent(): IdentifierInterface
{
return $this->parent;
}
/**
* @param IdentifierInterface $identifier
* @return bool
* @phpstan-pure
*/
public function hasIdentifier(IdentifierInterface $identifier): bool
{
return $this->getIdentifier($identifier->getId(), $identifier->getType()) !== null;
}
/**
* @return int
* @phpstan-pure
*/
abstract public function count(): int;
/**
* @return void
* @phpstan-pure
*/
public function check(): void
{
$min = $this->options['min'] ?? 0;
$max = $this->options['max'] ?? 0;
if ($min || $max) {
$count = $this->count();
if ($min && $count < $min) {
throw new \RuntimeException(sprintf('%s relationship has too few objects in it', $this->name));
}
if ($max && $count > $max) {
throw new \RuntimeException(sprintf('%s relationship has too many objects in it', $this->name));
}
}
}
/**
* @param IdentifierInterface $identifier
* @return IdentifierInterface
*/
private function checkIdentifier(IdentifierInterface $identifier): IdentifierInterface
{
if ($this->type !== $identifier->getType()) {
throw new \RuntimeException(sprintf('Bad identifier type %s', $identifier->getType()));
}
if (get_class($identifier) !== Identifier::class) {
return $identifier;
}
if ($this->type === 'media') {
return new MediaIdentifier($identifier->getId());
}
return new FlexIdentifier($identifier->getId(), $identifier->getType());
}
private function parseOptions(array $options): void
{
$this->type = $options['type'];
$this->options = $options;
}
}