How can I add simple CMS functionality to existing Symfony application - content-management-system

I have an existing web application accessing a MySQL database. I'm porting this application to Symfony. The new application has to use the old database, as we cannot port the whole application at once, i.e. the old and the new application are accessing the same database and the applications are running simultaneously.
The old application had a simple CMS functionality which has to be ported:
There is a table pagewhich represents a page tree. Every page has a slug field. The URL path consists of those slugs representing the path identifying the page node, e.g. "/[parent-slug]/[child-slug]".
The page table also contains a content field. As I already mentioned, the CMS functionality is very simple, so the content is just rendered as page content inside a page layout. The page entry also specifies the page layout / template.
My problem is that I don't know how to set up the routing. In a normal Symfony application I'd know the URL patterns before, but in this case they are dynamic. Also routes cannot be cached, because they could be changed any time by the user. I wonder if I have to drop Symfony's routing completely and implement something on my own. But how?
Now I found Symfony CMF which tells a lot about the framework VS CMS routing conflict. So first, I thought this would be the right way. However the tutorials aim at building an entirely new application based on PHPRC. I wasn't able to derive the tutorial's concepts to my use case.

since you run several URL rules on one symfony application, you will need to work with url prefixes. Either your cms should work with a prefix /cms/parent-slug/child-slug or all other controllers. Otherwise you are not able to differ which controller is meant when a dynamic request arrives.
You can try a workaround with a KernelControllerListener. He will catch up every request and then check if a cms page is requested. On the basis of the request you can set controller and action by yourself. Concept:
Create only one route with "/". Abandon oll other rules. Then create a Listener like this:
<?php
namespace AppBundle\Listener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* Class KernelControllerListener
* #package ApiBundle\Listener
*/
class KernelControllerListener
{
/**
* #var CmsRepository
*/
private $requestParser;
/**
* KernelControllerListener constructor.
* #param CmsRepository $CmsRepository
*/
public function __construct(CmsRepository $CmsRepository)
{
$this->CmsRepository = $CmsRepository;
}
/**
* #param FilterControllerEvent $event
*/
public function onKernelController(FilterControllerEvent $event){
$request = $event->getRequest();
//should be /parent-slug/children/slug or any other path
$path = $request->getPathInfo();
if($this->CmsRepository->getCmsControllerIfMatch($path)){
//cms repository search in db for page with this path, otherwise return false
$event->setController([AppBundle\CmsController::class, 'cmsAction']);
return;
}
//repeat if clause for any other application part
}
}
in services.yml:
app.controller_listener:
class: AppBundle\Listener\KernelControllerListener
arguments:
- "#app.cms_repository"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Edit: catch all routes, see https://www.jverdeyen.be/symfony2/symfony-catch-all-route/

The question is: Do you whant to migrate the data or not. For both question, the CMF can be an answer. If you wanna a simple dynamic router, you should have a look into the ChainRouter with an custom router definition:
https://symfony.com/doc/current/cmf/bundles/routing/dynamic.html
and
https://symfony.com/doc/current/cmf/components/routing/chain.html
If you wanna migrate the data, you can use fixture loaders, as we use in almost all of our examples.

Related

TYPO3 v10: How to access TSFE in Backend/Scheduler Task?

The current situation:
I am trying to access the TypoScript configuration of the frontend from within the backend (or rather a scheduler task). Previously with Typo3 v8 and v9, I initialized entire $GLOBALS["TSFE"] object, however this was already hack the last time around (using mostly deprecated calls) and now it has all been removed with the v10 release.
My goal:
Access the TypoScript configuration of the frontend of a certain page (root page of a site would be fine) from within a scheduler job.
Background of the whole project:
I have a periodic scheduler job that sends emails to various users (fe_users). The email contains links to certain pages (configured UIDs in typoscript) as well as file attachments and the likes (generated by other extensions, which are also fully configured via typoscript). Currently, I basically initialize the entire frontend from within the backend, but as I said before, its inefficient, super hacky and I doubt it was the intended way to solve this problem.
Getting TypoScript settings in the backend is ugly, but possible.
You need a page ID and a rootline which you can pass to \TYPO3\CMS\Core\TypoScript\TemplateService::runThroughTemplates().
Something along these lines:
$template = GeneralUtility::makeInstance(TemplateService::class);
$template->tt_track = false;
$rootline = GeneralUtility::makeInstance(
RootlineUtility::class, $pageId
)->get();
$template->runThroughTemplates($rootline, 0);
$template->generateConfig();
$typoScriptSetup = $template->setup;
You can get inspiration from \TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::getTypoScriptSetup and \TYPO3\CMS\Tstemplate\Controller\TypoScriptTemplateObjectBrowserModuleFunctionController
This won't get any better and is not intended to be done such way. I would use as configuration:
plain PHP, e.g. in $GLOBALS['TYPO3_CONF_VARS]`
YAML site config if depending on various sites
You can build links by using e.g. something like that
protected function generateUrl(int $pageId, int $recordId)
{
$additionalParams = 'tx_xxxx[action]=show&tx_ixxxx[controller]=Job&tx_xxxx[job]=' . $recordId;
return BackendUtility::getPreviewUrl($pageId, '', null, '', '', $additionalParams);
}

How to generate a rating meta tag in TYPO3 with caching?

I would like to generate a rich snippet for ratings in my TYPO3 pages. The rating information are fetched via an API, so I need some kind of caching mechanism.
I have some basic knowledge and experience with TYPO3 extensions, but I am not sure about the cleanest solution. I could render the meta tags with the TYPO3 Meta Tag API and cache the fetched information using the TYPO3 Caching Framework.
But I am not sure where to store the logic so that it gets executed at every page visit. I do not want to use a content plugin for obvious reasons. Should I set up a Controller and call the Contoller's function with e.g. some hook?
Thanks for any help!
You may have a look at the tslib/class.tslib_fe.php hooks in TypoScriptFrontendController class.
Choose the correct one (maybe tslib_fe-PostProc) and do something like this :
1) Register hook in ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['tslib_fe-PostProc'][] = \Vendor\Name\Hook\AdditionalMetaData::class . '->addRating';
2) Create your class with the following method
<?php
namespace Vendor\Name\Hook;
class AdditionalMetaData
{
/**
* #param array $parameters
* #return void
*/
public function addRating(&$parameters)
{
/** #var TypoScriptFrontendController $tsfe */
$tsfe = &$parameters['pObj'];
// Do your logic here...
}
}

Returning Array in USER_INT userFunc leads to <!--INT_SCRIPT output

I have a userFunc which I call via
lib.random = USER_INT
lib.random {
userFunc = My\Plugin\UserFunc\Functions->random
}
when I return a Array and try to access it is fails.
<v:variable.set name="random" value="{f:cObject(typoscriptObjectPath: 'lib.random')}" />
{random.max}
When I try to debug out it I get some <!--INT_SCRIPT string
Did anyone know the problem and a Solution?
/e:
I would like to make the problem a little clearer by describing the Szenario.
I have a Plugin with a Login form. When the User logs in I set a JWT with various basic informations (name, email).
This Informations have to be displayed on various places around the Website, not only on one page (for example profile page). Some cases are prefilled forms or just silly "Hello, Paul" stuff.
So when I first log in (Fresh browser, no cache) then I read "Hello, Paul" after I log out and log in with a another Account (Lets call it "Peter") then It still is written "Hello, Paul" , nor "Hello, Peter". When I clear my browser Cache then everything is fine.
Maybe this helps maybe to solve my dilemma. :)
TL;DR: uncached parts in TYPO3 are replaced in the generated page output string using markers and cannot communicate in the direction intended here. Selectively caching, disabling cache or detaching the data from the main request (with XHR or other) are the only possible methods.
It should be clear that USER_INT achieves its functionality by string replacement in the generated page body. This means, among other things, that:
You can never pass the output of a USER_INT to anything in Fluid, not even if the entire page is uncached. You will effectively be passing a string containing <!---INT_SCRIPT... (the entire marker).
You can however generate USER_INT from Fluid, which ends up in the generated page, which is then replaced with the rendered object (use f:cObject to render a USER_INT or COA_INT).
Then there are the use case context considerations. First of all, a cookie (in practice) changes the request parameters and should be part of the cache identifier that your page uses (it is not this way by default). Second, if said cookie changes the way the page renders (and it does, given your use case) this will cause trouble when the page is cached. Third, the page output changing based on a cookie indicates perhaps sensitive information or at the very least user-specific information.
Taking the above into account your use case should do one of the following things:
Either render the entire chunk of output that changes based on cookie, as USER_INT. That means wrapping the entire Fluid output and rendering it all without caching. Note that template compiling still happens (and you can use f:cache.static to hard-cache certain parts if they never change based on request parameters).
Or add the cookie value to the cHash (page hash value) so that having the cookie set means you request a specific cached version that matches the cookie. This is the preferred way if your cookie's values is generally the same for many users (e.g. it contains a selected contact person from a limited list and stores that in a cookie).
Or, in the case that your output contains actually sensitive information, require that the content element or page is only available when logged in with a specific group. This has two purposes: first, it protects the page from being viewed without authentication - but second, it also makes the page content not cache or be cached with the frontend user group ID as part of the cache identity.
Refactor to XHR request and make whichever endpoint it uses, a USER_INT or manually disabled cache context, then load the data. Or set the actual data in the cookie, then use JS to insert the values where needed.
Hopefully that clarifies the different contexts and why they can't communicate in the direction you're attempting; even if they had been exchanging strings instead of arrays.
See also: .cache sub-object in TypoScript which is where you would be able to craft a unique cache identifier for use case 2 described above.
USER_INT are not Cached, so the values for this are replaced after the cache is build up.
I think f:cObject is the wrong way. Implement an own ViewHelper to get the same data should be an better way.
<?php
namespace My\Plugin\ViewHelpers;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
class RandomViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;
/**
* #var boolean
*/
protected $escapeOutput = false;
/**
* #param array $arguments
* #param \Closure $renderChildrenClosure
* #param RenderingContextInterface $renderingContext
* #return string
*/
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
return rand();
}
}
Now you can use it like following:
{my:random()} or <my:random />

Implementation of Event-Sourcing / CQRS approach in api-platform

on official Api-Platform website there is a General Design Considerations page.
Last but not least, to create Event Sourcing-based systems, a convenient approach is:
to persist data in an event store using a custom data persister
to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
to map those projections with read-only Doctrine entity classes and to mark those classes with #ApiResource
You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.
So, I tried to implement this approach with one simplification (one DB is used, but with separated reads and writes).
But failed... there is a problem, which I don't know how to resolve, so kindly asking you for a help!
I created a User Doctrine entity and annotated fields I want to expose with #Serializer\Groups({"Read"}). I will omit it here as it's very generic.
User resource in yaml format for api-platform:
# config/api_platform/entities/user.yaml
App\Entity\User\User:
attributes:
normalization_context:
groups: ["Read"]
itemOperations:
get: ~
collectionOperations:
get:
access_control: "is_granted('ROLE_ADMIN')"
So, as it's shown above User Doctrine entity is read-only, as only GET methods are defined.
Then I created a CreateUser DTO:
# src/Dto/User/CreateUser.php
namespace App\Dto\User;
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
final class CreateUser
{
/**
* #var string
* #Assert\NotBlank()
* #Assert\Email()
* #AppAssert\FakeEmailChecker()
*/
public $email;
/**
* #var string
* #Assert\NotBlank()
* #AppAssert\PlainPassword()
*/
public $plainPassword;
}
CreateUser resource in yaml format for api-platform:
# config/api_platform/dtos/create_user.yaml
App\Dto\User\CreateUser:
itemOperations: {}
collectionOperations:
post:
access_control: "is_anonymous()"
path: "/users"
swagger_context:
tags: ["User"]
summary: "Create new User resource"
So, here you can see that only one POST method is defined, exactly for creation of a new User.
And here what router shows:
$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name Method Scheme Host Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection POST ANY ANY /users
api_users_get_collection GET ANY ANY /users.{_format}
api_users_get_item GET ANY ANY /users/{id}.{_format}
I also added a custom DataPersister to handle POST to /users. In CreateUserDataPersister::persist I used Doctrine entity to write data, but for this case it doesn't matter as Api-platform do not know anything about how DataPersister will write it.
So, from the concept - it's a separation of reads and writes.
Reads are performed by Doctrine's DataProvider shipped with Api-platform, and writes are performed by custom DataPersister.
# src/DataPersister/CreateUserDataPersister.php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;
class CreateUserDataPersister implements DataPersisterInterface
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function supports($data): bool
{
return $data instanceof CreateUser;
}
public function persist($data)
{
$user = new User();
$user
->setEmail($data->email)
->setPlainPassword($data->plainPassword);
$this->manager->persist($user);
$this->flush();
return $user;
}
public function remove($data)
{
}
}
When I perform a request to create new User:
POST https://{{host}}/users
Content-Type: application/json
{
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
Problem!
I'm getting a 400 response ... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...
However, a new record is added to database, so custom DataPersister works ;)
According to General Design Considerations separations of writes and reads are implemented, but not working as expected.
I'm pretty sure, that I could be missing something to configure or implement. So, that's why it's not working.
Would be happy to get any help!
Update 1:
The problem is in \ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName(). At lines 48-59 it iterates through all routes trying to find appropriate route for:
$operationType = 'item'
$resourceClass = 'App\Dto\User\CreateUser'
But $operationType = 'item' is defined only for $resourceClass = 'App\Entity\User\User', so it fails to find the route and throws an exception.
Update 2:
So, the question could sound like this:
How it's possible to implement separation of reads and writes (CQS?) using Doctrine entity for reads and DTO for writes, both residing on the same route, but with different methods?
Update 3:
Data Persisters
store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
not publicly expose the internal model mapped with the database through the API
use a separate model for read operations and for updates by implementing patterns such as CQRS
Yes! I want that... but how to achieve it in my example?
Short Answer
The problem is that the Dto\User\CreateUser object is getting serialized for the response, when in fact, you actually want the Entity\User to be returned and serialized.
Long Answer
When API Platform serializes a resource, they will generate an IRI for the resource. The IRI generation is where the code is puking. The default IRI generator uses the Symfony Router to actually build the route based on the API routes created by API Platform.
So for generating an IRI on an entity, it will need to have a GET item operation defined because that is the route that will be the IRI for the resource.
In your case, the DTO doesn't have a GET item operation (and shouldn't have one), but when API Platform tries to serialize your DTO, it throws that error.
Steps to Fix
From your code sample, it looks like the User is being returned, however, it's clear from the error that the User entity is not the one being serialized.
One thing to do would be to install the debug-pack, start the dump server with bin/console server:dump, and add a few dump statements in the API Platform WriteListener: ApiPlatform\Core\EventListener\WriteListener near line 53:
dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);
The Controller Result should be an instance of your DTO, the Persist Result should be an instance of your User entity, but I'm guessing it's returning your DTO.
If it is returning your DTO, you need to just debug and figure out why the DTO is being returned from the dataPersister->persist instead of the User entity. Maybe you have other data persisters or things in your system that can be causing conflict.
Hopefully this helps!
You need to send the "id" in your answer.
If User is Doctrine entity, use:
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
If User isn't Doctrine entity, use:
/**
* #Assert\Type(type="integer")
* #ApiProperty(identifier=true)
*/
private $id;
Anyway, your answer would be like this:
{
"id": 1, // Your unique id of User
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
P.S.: sorry for my english :)
Only Work in the 2.4 version but really helpful.
Just add
output_class=false for the CreateUserDTO and everything will be fine for POST|PUT|PATCH
output_class to false allow you to bypass the get item operation. You can see that in the ApiPlatform\Core\EventListener#L68.

Zend Framework website.com/username

One of the application I am developing using Zend Framework requires the user's profile page to be accessed via website.com/username, while other pages should be accessed by website.com/controller_name/action_name
I am not too sure how can this be achieved, however, I feel this can be done with some tweaks in the .htaccess file.
Can someone here please help me out?
Many thanks in advance
As suggested before, you can use a custom route that will route single level requests. However, this will also override the default route. If you're using modules, this will no longer work example.com/<module>.
I have done this before but only for static pages. I wanted this:
example.com/about
instead of this:
example.com/<some-id>/about
while maintaining the default route so this still works
example.com/<module>
example.com/<controller>
The way I did this was using a plugin to test if my request could be dispatched. If the request could not be dispatched using the default route, then I would change the request to the proper module to load my page. Here is a sample plugin:
class My_Controller_Plugin_UsernameRoute extends Zend_Controller_Plugin_Abstract
{
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();
if (!$dispatcher->isDispatchable($request)) {
$username = $request->getControllerName();
$request->setModuleName('users');
$request->setControllerName('dashboard');
$request->setActionName('index');
$request->setParam('username', $username);
/** Prevents infinite loop if you make a mistake in the new request **/
if ($dispatcher->isDispatchable($request)) {
$request->setDispatched(false);
}
}
}
}
What about using Zend_Controller_Router_Route, look here the link http://framework.zend.com/manual/en/zend.controller.router.html#zend.controller.router.routes.standard.variable-requirements