diff --git a/config/services.php b/config/services.php index e9560bdf..39581b2b 100644 --- a/config/services.php +++ b/config/services.php @@ -16,6 +16,7 @@ use Stenope\Bundle\Command\BuildCommand; use Stenope\Bundle\Command\DebugCommand; use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Stenope\Bundle\Decoder\HtmlDecoder; use Stenope\Bundle\Decoder\MarkdownDecoder; use Stenope\Bundle\DependencyInjection\tags; @@ -70,7 +71,8 @@ '$propertyAccessor' => service('property_accessor'), '$expressionLanguage' => service(ExpressionLanguage::class)->nullOnInvalid(), '$stopwatch' => service('debug.stopwatch')->nullOnInvalid(), - ]) + ])->call('setContentManager', [service(ContentManagerInterface::class)]) + ->alias(ContentManagerInterface::class, ContentManager::class) // Content providers factories ->set(ContentProviderFactory::class)->args(['$factories' => tagged_iterator(tags\content_provider_factory)]) @@ -78,7 +80,7 @@ // Debug ->set(DebugCommand::class)->args([ - '$manager' => service(ContentManager::class), + '$manager' => service(ContentManagerInterface::class), '$stopwatch' => service('stenope.build.stopwatch'), ]) ->tag('console.command', ['command' => DebugCommand::getDefaultName()]) @@ -164,7 +166,7 @@ // Symfony HttpKernel controller argument resolver ->set(ContentArgumentResolver::class) - ->args(['$contentManager' => service(ContentManager::class)]) + ->args(['$contentManager' => service(ContentManagerInterface::class)]) ->tag('controller.argument_value_resolver', [ 'priority' => 110, // Prior to RequestAttributeValueResolver to resolve from route attribute ]) @@ -172,7 +174,7 @@ // Twig ->set(ContentExtension::class)->tag('twig.extension') ->set(ContentRuntime::class) - ->args(['$contentManager' => service(ContentManager::class)]) + ->args(['$contentManager' => service(ContentManagerInterface::class)]) ->tag('twig.runtime') // Assets diff --git a/doc/app/src/Controller/DocController.php b/doc/app/src/Controller/DocController.php index 43d28ecf..0decc660 100644 --- a/doc/app/src/Controller/DocController.php +++ b/doc/app/src/Controller/DocController.php @@ -4,15 +4,15 @@ use App\Model\Index; use App\Model\Page; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; class DocController extends AbstractController { - private ContentManager $contentManager; + private ContentManagerInterface $contentManager; - public function __construct(ContentManager $contentManager) + public function __construct(ContentManagerInterface $contentManager) { $this->contentManager = $contentManager; } diff --git a/doc/loading-content.md b/doc/loading-content.md index 2dd67555..1e8920a8 100644 --- a/doc/loading-content.md +++ b/doc/loading-content.md @@ -68,7 +68,7 @@ In your controller (or service): namespace App\Controller; use App\Model\Article; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; @@ -80,7 +80,7 @@ class BlogController extends AbstractController /** * @Route("/", name="blog") */ - public function index(ContentManager $contentManager) + public function index(ContentManagerInterface $contentManager) { return $this->render( 'blog/index.html.twig', @@ -93,7 +93,7 @@ _Note: contents of the same type can very well be writen in different formats._ ### Fetching a specific content -The ContentManager uses slugs to identify your content. +The content manager uses slugs to identify your content. The `slug` argument must exactly match the static file name in your content directory. @@ -105,7 +105,7 @@ Example: `$contentManager->getContent(Article::class, 'how-to-train-your-dragon' namespace App\Controller; use App\Model\Article; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; @@ -119,7 +119,7 @@ class BlogController extends AbstractController /** * @Route("/{slug}", name="article") */ - public function article(ContentManager $contentManager, string $slug) + public function article(ContentManagerInterface $contentManager, string $slug) { return $this->render( 'blog/article.html.twig', @@ -194,7 +194,7 @@ $myDrafts = $contentManager->getContents( #### A custom callable supported by the PHP [usort](https://www.php.net/manual/fr/function.usort.php) function ```php -$tagedMobileArticles = $contentManager->getContents( +$taggedMobileArticles = $contentManager->getContents( Article::class, null, fn (Article $article): bool => in_array('mobile', $article->tags) @@ -206,7 +206,7 @@ $tagedMobileArticles = $contentManager->getContents( ```php use function Stenope\Bundle\ExpressionLanguage\expr; -$tagedMobileArticles = $contentManager->getContents( +$taggedMobileArticles = $contentManager->getContents( Article::class, null, expr('"mobile" in _.tags') diff --git a/doc/twig.md b/doc/twig.md index ba886cc5..4f6c69f2 100644 --- a/doc/twig.md +++ b/doc/twig.md @@ -1,6 +1,6 @@ # Twig integration -Stenope provides a Twig extension to help you interact with the `ContentManager` +Stenope provides a Twig extension to help you interact with the `ContentManagerInterface` from your templates. ## Functions diff --git a/src/Behaviour/ContentManagerAwareInterface.php b/src/Behaviour/ContentManagerAwareInterface.php index 31f37f75..503682fd 100644 --- a/src/Behaviour/ContentManagerAwareInterface.php +++ b/src/Behaviour/ContentManagerAwareInterface.php @@ -8,12 +8,12 @@ namespace Stenope\Bundle\Behaviour; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; interface ContentManagerAwareInterface { /** * Sets the owning ContentManager object. */ - public function setContentManager(ContentManager $contentManager); + public function setContentManager(ContentManagerInterface $contentManager); } diff --git a/src/Behaviour/ContentManagerAwareTrait.php b/src/Behaviour/ContentManagerAwareTrait.php index 4a597b64..71417353 100644 --- a/src/Behaviour/ContentManagerAwareTrait.php +++ b/src/Behaviour/ContentManagerAwareTrait.php @@ -8,13 +8,16 @@ namespace Stenope\Bundle\Behaviour; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; +/** + * @see ContentManagerAwareInterface + */ trait ContentManagerAwareTrait { - private ContentManager $contentManager; + private ContentManagerInterface $contentManager; - public function setContentManager(ContentManager $contentManager): void + public function setContentManager(ContentManagerInterface $contentManager): void { $this->contentManager = $contentManager; } diff --git a/src/Command/DebugCommand.php b/src/Command/DebugCommand.php index 2eaf19bc..42341a6b 100644 --- a/src/Command/DebugCommand.php +++ b/src/Command/DebugCommand.php @@ -8,7 +8,7 @@ namespace Stenope\Bundle\Command; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use function Stenope\Bundle\ExpressionLanguage\expr; use Stenope\Bundle\ExpressionLanguage\Expression; use Stenope\Bundle\TableOfContent\Headline; @@ -31,10 +31,10 @@ class DebugCommand extends Command protected static $defaultName = 'debug:stenope:content'; - private ContentManager $manager; + private ContentManagerInterface $manager; private Stopwatch $stopwatch; - public function __construct(ContentManager $manager, Stopwatch $stopwatch) + public function __construct(ContentManagerInterface $manager, Stopwatch $stopwatch) { $this->manager = $manager; $this->stopwatch = $stopwatch; diff --git a/src/ContentManager.php b/src/ContentManager.php index 17dbcb7a..2ee57689 100644 --- a/src/ContentManager.php +++ b/src/ContentManager.php @@ -26,7 +26,7 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Stopwatch\Stopwatch; -class ContentManager +class ContentManager implements ContentManagerInterface { private DecoderInterface $decoder; private DenormalizerInterface $denormalizer; @@ -51,6 +51,8 @@ class ContentManager private bool $managerInjected = false; + private ?ContentManagerInterface $contentManager; + public function __construct( DecoderInterface $decoder, DenormalizerInterface $denormalizer, @@ -76,17 +78,7 @@ public function __construct( } /** - * List all content for the given type - * - * @template T - * - * @param class-string $type Model FQCN e.g. "App/Model/Article" - * @param string|array|callable $sortBy String, array or callable - * @param string|array|callable|Expression $filterBy Array, callable or an {@link Expression} instance / string - * to filter out with an expression using the ExpressionLanguage - * component. - * - * @return array List of decoded contents with their slug as key + * {@inheritdoc} */ public function getContents(string $type, $sortBy = null, $filterBy = null): array { @@ -120,14 +112,14 @@ public function getContents(string $type, $sortBy = null, $filterBy = null): arr return $contents; } - public function filterBy(array &$contents, $filterBy = null): void + private function filterBy(array &$contents, $filterBy = null): void { if ($filter = $this->getFilterFunction($filterBy)) { $contents = array_filter($contents, $filter); } } - public function sortBy(array &$contents, $sortBy = null): void + private function sortBy(array &$contents, $sortBy = null): void { if ($sorter = $this->getSortFunction($sortBy)) { \set_error_handler(static function (int $severity, string $message, ?string $file, ?int $line): void { @@ -141,14 +133,7 @@ public function sortBy(array &$contents, $sortBy = null): void } /** - * Fetch a specific content - * - * @template T - * - * @param class-string $type Model FQCN e.g. "App/Model/Article" - * @param string $id Unique identifier (slug) - * - * @return T An object of the given type. + * {@inheritdoc} */ public function getContent(string $type, string $id): object { @@ -171,6 +156,9 @@ public function getContent(string $type, string $id): object throw new ContentNotFoundException($type, $id); } + /** + * {@inheritdoc} + */ public function reverseContent(Context $context): ?Content { $key = md5(serialize($context)); @@ -345,10 +333,23 @@ private function initProcessors(): void if (!$this->managerInjected) { foreach ($this->processors as $processor) { if ($processor instanceof ContentManagerAwareInterface) { - $processor->setContentManager($this); + $processor->setContentManager($this->contentManager ?? $this); } } $this->managerInjected = true; } } + + /** + * Set the actual content manager instance to inject in processors. + * Useful whenever this content manager is decorated in order for the processor to use the decorating one. + */ + public function setContentManager(ContentManagerInterface $contentManager): void + { + if ($contentManager === $this) { + return; + } + + $this->contentManager = $contentManager; + } } diff --git a/src/ContentManagerInterface.php b/src/ContentManagerInterface.php new file mode 100644 index 00000000..15dbc8d7 --- /dev/null +++ b/src/ContentManagerInterface.php @@ -0,0 +1,50 @@ + + */ + +namespace Stenope\Bundle; + +use Stenope\Bundle\ReverseContent\Context; +use Symfony\Component\ExpressionLanguage\Expression; + +interface ContentManagerInterface +{ + /** + * List all content for the given type + * + * @template T + * + * @param class-string $type Model FQCN e.g. "App/Model/Article" + * @param string|array|callable $sortBy String, array or callable + * @param string|array|callable|Expression $filterBy Array, callable or an {@link Expression} instance / string + * to filter out with an expression using the ExpressionLanguage + * component. + * + * @return array List of decoded contents with their slug as key + */ + public function getContents(string $type, $sortBy = null, $filterBy = null): array; + + /** + * Fetch a specific content + * + * @template T + * + * @param class-string $type Model FQCN e.g. "App/Model/Article" + * @param string $id Unique identifier (slug) + * + * @return T An object of the given type. + */ + public function getContent(string $type, string $id): object; + + /** + * Attempt to reverse resolve a content according to a context. + * E.g: attempt to resolve a content relative to another one through its filesystem path. + */ + public function reverseContent(Context $context): ?Content; + + public function supports(string $type): bool; +} diff --git a/src/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolver.php b/src/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolver.php index 2cd3e774..12e92231 100644 --- a/src/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolver.php +++ b/src/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolver.php @@ -8,16 +8,16 @@ namespace Stenope\Bundle\HttpKernel\Controller\ArgumentResolver; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; class ContentArgumentResolver implements ArgumentValueResolverInterface { - private ContentManager $contentManager; + private ContentManagerInterface $contentManager; - public function __construct(ContentManager $contentManager) + public function __construct(ContentManagerInterface $contentManager) { $this->contentManager = $contentManager; } diff --git a/src/ReverseContent/Context.php b/src/ReverseContent/Context.php index eb1971df..5ce81e76 100644 --- a/src/ReverseContent/Context.php +++ b/src/ReverseContent/Context.php @@ -8,12 +8,12 @@ namespace Stenope\Bundle\ReverseContent; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; /** * Context from which to resolve a content. * - * @see ContentManager::reverseContent() + * @see ContentManagerInterface::reverseContent() */ abstract class Context { diff --git a/src/Twig/ContentRuntime.php b/src/Twig/ContentRuntime.php index 5cd11b6b..9bcd1f99 100644 --- a/src/Twig/ContentRuntime.php +++ b/src/Twig/ContentRuntime.php @@ -8,14 +8,14 @@ namespace Stenope\Bundle\Twig; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Twig\Extension\RuntimeExtensionInterface; class ContentRuntime implements RuntimeExtensionInterface { - private ContentManager $contentManager; + private ContentManagerInterface $contentManager; - public function __construct(ContentManager $contentManager) + public function __construct(ContentManagerInterface $contentManager) { $this->contentManager = $contentManager; } diff --git a/tests/Unit/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolverTest.php b/tests/Unit/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolverTest.php index c09f096b..fffac497 100644 --- a/tests/Unit/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolverTest.php +++ b/tests/Unit/HttpKernel/Controller/ArgumentResolver/ContentArgumentResolverTest.php @@ -12,7 +12,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Stenope\Bundle\Content; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Stenope\Bundle\HttpKernel\Controller\ArgumentResolver\ContentArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -23,13 +23,13 @@ class ContentArgumentResolverTest extends TestCase private ContentArgumentResolver $resolver; - /** @var ContentManager|ObjectProphecy */ + /** @var ContentManagerInterface|ObjectProphecy */ private ObjectProphecy $manager; protected function setUp(): void { $this->resolver = new ContentArgumentResolver( - ($this->manager = $this->prophesize(ContentManager::class))->reveal() + ($this->manager = $this->prophesize(ContentManagerInterface::class))->reveal() ); } diff --git a/tests/Unit/Processor/ResolveContentLinksProcessorTest.php b/tests/Unit/Processor/ResolveContentLinksProcessorTest.php index c63e2a24..2dbdfd7f 100644 --- a/tests/Unit/Processor/ResolveContentLinksProcessorTest.php +++ b/tests/Unit/Processor/ResolveContentLinksProcessorTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Stenope\Bundle\Content; -use Stenope\Bundle\ContentManager; +use Stenope\Bundle\ContentManagerInterface; use Stenope\Bundle\Processor\ResolveContentLinksProcessor; use Stenope\Bundle\ReverseContent\RelativeLinkContext; use Stenope\Bundle\Routing\ContentUrlResolver; @@ -44,7 +44,7 @@ public function testResolveLinks(): void $processor = new ResolveContentLinksProcessor($urlGenerator->reveal(), new NaiveHtmlCrawlerManager()); - $manager = $this->prophesize(ContentManager::class); + $manager = $this->prophesize(ContentManagerInterface::class); $processor->setContentManager($manager->reveal()); $manager->reverseContent(new RelativeLinkContext(