How to get child object in embedded Admin class in Sonata Admin? - symfony

I'm trying to get and manipulate the actual object related to a ImageAdmin class in SonataAdmin (using Symfony 2.3). This works fine when the ImageAdmin class is the only one being used. But when ImageAdmin is embedded in another Admin it goes horribly wrong.
Here's what works when you don't have embedded Admins:
class ImageAdmin extends Admin {
protected $baseRoutePattern = 'image';
protected function configureFormFields(FormMapper $formMapper) {
$subject = $this->getSubject();
}
}
But when you embed ImageAdmin in ParentAdmin using this:
class PageAdmin extends Admin {
protected function configureFormFields(FormMapper $formMapper) {
$formMapper->add('image1', 'sonata_type_admin');
}
}
Then when you're editing a Parent item with id 10 and call getSubject() in ImageAdmin you get the Image with id 10!
In other words getSubject() extracts the id from the URL then calls $this->getModelManager()->find($this->getClass(), $id);, which cross-references the Parent id and the Image id. Oops!
So... what I want to do is be able to get hold of the actual object that is being rendered/edited in the current ImageAdmin instance, whether it's being edited directly or via an embedded form, and then be able to do things with it.
Maybe getSubject() is the wrong tree to be barking up, but I note that $this->getCurrentChild() returns false when called from ImageAdmin::configureFormFields(), even when that ImageAdmin is embedded using the sonata_type_admin field type. I'm quite confused...
Anyway, I hope it is possible to get hold of the object in some obvious way that I've overlooked and somebody here can help enlighten me!

Thanks to Tautrimas for some ideas, but I managed to figure out an answer to this:
In ImageAdmin set this:
protected function configureFormFields(FormMapper $formMapper)
{
if($this->hasParentFieldDescription()) { // this Admin is embedded
$getter = 'get' . $this->getParentFieldDescription()->getFieldName();
$parent = $this->getParentFieldDescription()->getAdmin()->getSubject();
if ($parent) {
$image = $parent->$getter();
} else {
$image = null;
}
} else { // this Admin is not embedded
$image = $this->getSubject();
}
// You can then do things with the $image, like show a thumbnail in the help:
$fileFieldOptions = array('required' => false);
if ($image && ($webPath = $image->getWebPath())) {
$fileFieldOptions['help'] = '<img src="'.$webPath.'" class="admin-preview" />';
}
$formMapper
->add('file', 'file', $fileFieldOptions)
;
}
I'll post this in the upcoming SonataAdmin cookbook soon!
https://github.com/sonata-project/SonataAdminBundle/issues/1546

caponica's solution is working only on oneToOne relations, am I right? In my oneToMany case , this: $parent->$getter() returns a collection, and I don't know how to identify the current subject.
I've found this bug report:
https://github.com/sonata-project/SonataAdminBundle/issues/1568, which contains a fix for this, but it is still open, so I hope they merge it soon:(
Edit
With some research there is a temporary fix for this: Fixed getting wrong subject in sonata_type_collection
In short:
create a class and copypaste the content of this file: AdminType
then add this to your services.yml, and change the class namespace to you new class namespace:
services:
sonata.admin.form.type.admin:
class: ACME\AdminBundle\Form\Type\AdminType
tags:
- { name: form.type, alias: sonata_type_admin }
It still has a bug though:
also fix doesn't work when enabled cascade_validation in the parent docment and embedded form has errors

Can you try $this->getForm()->getViewData(); within your ImageAdmin? This should get you the correct child entity.

I tried all these solutions, but none proved to work.
So, I worked to find a solution. My solution is based on caponica's solution, but work on oneToMany case. Tha solution I found is a workaround, but works good.
It's working using the session.
public function getCurrentObjectFromCollection($adminChild)
{
$getter = 'get' . $adminChild->getParentFieldDescription()
->getFieldName();
$parent = $adminChild->getParentFieldDescription()
->getAdmin()
->getSubject();
$collection = $parent->$getter();
$session = $adminChild->getRequest()->getSession();
$number = 0;
if ($session->get('adminCollection')) {
$number = $session->get('adminCollection');
$session->remove('adminCollection');
}
else {
$session->set('adminCollection', 1 - $number);
}
return $collection[$number];
}
And you get the correct object in the admin by:
$object = $this->getCurrentObjectFromCollection($this)
So, when the parent needs to show the list of child admins, each child admin will run this function and will update the session parameter. When all the elements have been taken, the session parameter is deleted.
This code is made for lists with only 2 elements, but can be updated for any number of elements.
Hope this helps somebody :)

I had same problem and i am able to do this through "Custom Form Type Extension" for which documentation is given on the link "http://symfony.com/doc/current/cookbook/form/create_form_type_extension.html" .
It is the perfect solution ..

Related

Silverstripe 4 save variable in database

Is posible to save SS template variable in database from CMS and after execute it in template?
Okay lets see example:
In CMS i have settings where i put social media links and contact informatios.
Also in CMS i have module where i create HTML block-s which after that i loop in website.
In that html block i want to put existing $SiteConfig.Email variable.
I Try that but that is rendered in template like $SiteConfig.Email not show real email?
Is this posible to do or i need some extra modification?
Check photo
The question you have written makes no sense to me, but I understand the screenshot.
So, SilverStripe renders .ss files with a class called SSViewer. Basically it reads the file as string and then runs it through SSViewer to generate the HTML output.
But, as you saw, the output of variables is not processed.
I can think of 3 ways to get what you want:
Run the variables through SSViewer aswell (in this example, use $RenderedHTMLContent in the template)
class MyDataObject extends DataObject {
private static array $db = [
'Title' => DBVarchar::class,
'HTMLContent' => DBText::class,
];
public function Foobar() { return "hello from foobar"; }
public function RenderedHTMLContent() {
$template = \SilverStripe\View\SSViewer::fromString($this->HTMLContent);
// using $this->renderWith() will allow you access to all things of $this in the template. so eg $ID, $Title or $Foobar. Probably also $SiteConfig because it's global
return $this->renderWith($template);
// if you want to add extra variables that are not part of $this, you can also do:
return $this->renderWith($template, ["ExtraVariable" => "Hello from extra variable"]);
// if you do not want $this, you can do:
return (new ArrayData(["MyVariable" => "my value"]))->renderWith($template);
}
}
Please be aware of the security implications this thing brings. SilverStripe is purposely built to not allow content authors to write template files. A content author can not only call the currently scoped object but also all global template variables. This includes $SiteConfig, $List, .... Therefore a "bad" content author can write a template like <% loop $List('SilverStripe\Security\Member') %>$ID $FirstName $LastName $Email $Salt $Password<% end_loop %> or perhaps might access methods that have file access. So only do this if you trust your content authors
Use shortcodes instead of variables. But I never liked shortcodes, so I don't remember how they work. You'll have to lookup the docs for that.
Build your own mini template system with str_replace.
class MyDataObject extends DataObject {
private static array $db = [
'Title' => DBVarchar::class,
'HTMLContent' => DBText::class,
];
public function Foobar() { return "hello from foobar"; }
public function RenderedHTMLContent() {
return str_replace(
[
'$SiteConfig.Title',
'$SiteConfig.Tagline',
'$Title',
'$Foobar',
],
[
SiteConfig::current_site_config()->Title,
SiteConfig::current_site_config()->Tagline,
$this->Title,
$this->Foobar(),
],
$this->HTMLContent
);
}
}

Symfony Dynamic Form Constraints

Looking for a straightforward way to add constraints dynamically to all of my form fields. So far I've hit upon the idea of using a form type extension, which kind of works: I can modify the form view and then manually check the view on form submission.
However, is there a smarter way to add real Symfony-based constraints in real-time?
(Note that the constraints need to be added to the form in real-time as the form loads based on user configuration in the database.. Predefined form groups and the like won't work.)
I would suggest to use form events.
Use the PRE_SUBMIT event to edit the form before validation.
Recreate your fields with $event->getForm()->add(...) adding your constraints.
Of course you can automatically add the listener to all form using a FormExtension which adds the listener.
EDIT : Some examples from Alsatian67/FormBundle
Your extension should looks like :
class ExtensibleExtension extends AbstractTypeExtension
{
private $extensibleSubscriber;
public function __construct($extensibleSubscriber) {
$this->extensibleSubscriber = $extensibleSubscriber;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Only apply on base form
if($builder->getForm()->isRoot())
{
$builder->addEventSubscriber($this->extensibleSubscriber);
}
}
public function getExtendedType()
{
return FormType::class;
}
}
And your EventListener / EventSubscriber should iterate on all the children :
foreach($event->getForm()->all() as $child){
$childName = $child->getName();
$type = get_class($child->getConfig()->getType()->getInnerType());
$options = $child->getConfig()->getOptions();
$options['constraints'] = array(/* ... */);
$form->add($childName,$type,$options);
}

Symfony2 twig mobile template fallback

I need a simple way to fallback on a default template if no mobile version exists.
With some regular expressions I recognize mobile platforms and want to render a template with the following pattern:
<template_name>.mobile.html.twig
But if this template doesn't exist, I want it to automatically fallback on:
<template_name>.html.twig
which always exists.
I tried nearly all the answers from this post:
Symfony 2 load different template depending on user agent properties
but with no success. Unfortunately there are no version numbers referenced.
At the moment I am trying to copy and modify the default twig loader.
By the way, What I want to achieve with this is the possibility to deploy different templates for mobile devices by just adding a template of the same name and adding a .mobile.
UPDATE:
http://www.99bugs.com/handling-mobile-template-switching-in-symfony2/
This one is also a good approach. It modifies the format property of the request object which affects the automatic template guessing when you don't specify a template in the controller with the render function (or annotation) but just return an array.
Resulting template name:
view/<controller>/<action>.<request format>.<engine>
So you could switch the request format from html to mobile.html based on the device detection.
The downside of this is that every template needs a mobile.html pendant (which then could just include the non-mobile version if not needed).
UPDATE:
Besides using a custom templating provider there is also the possibility to hook into the kernel.view event.
You could create a service to handle it and then use it in the same way that you do the templating service like so..
Create a service with the templating and request service injected into it..
Service (YAML)
acme.templating:
class: Acme\AcmeBundle\Templating\TemplatingProvider
scope: request
arguments:
- #templating
- #request // I assume you use request in your platform decision logic,
// otherwise you don't needs this or the scope part
- 'html'
Class
class TemplatingProvider
{
private $fallback;
private $platform;
... __construct($templating, $request, $fallback) etc
private function setPlatform() ... Your platform decision logic
private function getPlatform()
{
if (null === $this->platform) {
$this->setPlatform();
}
return $this->platform;
}
private function getTemplateName($name, $platform)
{
if ($platform === 'html') {
return $name;
}
$template = explode('.', $name);
$template = array_merge(
array_slice($template, 0, -2),
array($platform),
array_slice($template, -2)
);
return implode('.', $template);
}
public function renderResponse($name, array $parameters = array())
{
$newname = $this->getTemplateName($name, $this->getPlatform());
if ($this->templating->exists($newname)) {
return $this->templating->render($newname);
}
return $this->templating->renderResponse($this->getTemplateName(
$name, $this->fallback));
}
And then you could just call your templating service instead of the current one..
return $this->container->get('acme.templating')
->renderResponse('<template_name>.html.twig', array());
Can't you check if the template exist before ?
if ( $this->get('templating')->exists('<templatename>.html.twig') ) {
// return this->render(yourtemplate)
} else {
// return your default template
}
OR :
You can create a generic method, to insert in your root controller like :
public function renderMobile($templateName, $params)
{
$templateShortName = explode('.html.twig', $templateName)[0];
$mobileName = $templateShortName.'.mobile.html.twig';
if ( $this->get('templating')->exists($mobileName) ) {
return $this->renderView($mobileName, $params);
} else {
return $this->renderView($templateName, $params)
}
}
with this you can do :
return $this->renderMobile('yourtemplate', [yourparams]);
You can easily do this by harnessing the bundle inheritance properties in Symfony2 http://symfony.com/doc/current/cookbook/bundles/inheritance.html
create a bundle which holds your desktop templates (AcmeDemoDesktopBundle)
create a bundle which will hold your mobile templates (AcmeDemoMobileBundle) and mark the parent as AcmeDemoDesktopBundle
Then when you render a template simply call AcmeDemoMobileBundle:: - if the template exists, it'll be rendered otherwise you'll neatly fall back to the desktop version. No extra code, listeners or anything none-obvious required.
The downside of this of course is that you move your templates out of the individual bundles.
The fallback behavior you describe isn't that easy to implement (we found out the hard way..). Good news is we wanted the same setup as you ask for and ended up using the LiipThemeBundle for this purpose. It allows you to have different "themes" based on for example a device. It will do the fallback part for you.
For example:
Rendering a template:
#BundleName/Resources/template.html.twig
Will render and fallback to in order:
app/Resources/themes/phone/BundleName/template.html.twig
app/Resources/BundleName/views/template.html.twig
src/BundleName/Resources/themes/phone/template.html.twig
src/BundleName/Resources/views/template.html.twig
Edit: so with this approach you can have default templates that will always be the final fallback and have a special template for mobile where you need it.

Zend Framework 2 - Add style to controller

I have been looking through the documentation, and I can't seem to find a way to do this. I know I can use headScript to add style sheets to individual views, but I would like to add a style sheet to all actions in a controller.
Has anyone done this? I am sure it is a simple task.
Thanks
What you need to do is hook into the dispatch event and, based on the type of controller that was dispatched, set the appropriate layout (recommended). You could also directly modify the view and add the required assets.
This can be achieved by using the following code in your Module class:
<?php
namespace App;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$event->getApplication()->getEventManager()->getSharedManager()->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, function (MvcEvent $event)
{
$application = $event->getApplication();
$services = $application->getServiceManager();
$view = $services->get('ViewRenderer');
$controller = $event->getTarget();
if ($controller instanceof \App\Controller\Entry)
{
$controller->layout('layout/app/entry');
// -- OR --
$view->headStyle()->appendStyle('body{background:red}');
}
}, 100);
}
}
I hope this answers your question!

Unit tests failing when I use a custom view helper in the layout

So I have created my custom view helper and used it in layout.phtml like this:
<?php echo $this->applicationBar(); ?>
It is working flawlessly in the browser but my unit tests that were working before are failing now:
1) UnitTests\Application\Controller\IndexControllerTest::testIndexActionCanBeAccessed
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for applicationBar
When I comment out the view helper in the layout file, test passes again.
I have the same problem, and i solved it in not a good way (but it solved for my specific problem).
The phpunit tests is not finding my factories view helpers, but it is finding my invokables. Then, i did the following:
public function getViewHelperConfig() {
return array(
'factories' => array(
'aplicationBar' => function($service) {
$applicationBar = new ApplicationBar();
return $applicationBar;
},
),
'invokables' => array(
'applicationBar' => 'Application\View\Helper\ApplicationBar',
),
);
When i use the browser, it uses the correct Factory. When i use phpunit, it uses the invokables.
The problem happens when i need to set some parameters. And then, i set some default parameters, that will be used only by phpunit.
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
class ApplicationBar extends AbstractHelper {
protected $parameter;
public function __construct($parameter = 'something') {
$this->parameter = $parameter;
}
public function __invoke() {
return $this->parameter;
}
}
It is not the best solution, but if i solve it in a better way, i will post here.

Resources