I created a new plugin, with a simple custom frontend route. The page object is null when printed, so the elements inside are gone. I know I can just pass it from the renderStorefront function as a second parameter, but don't know what to fill in it because usually it is filled automatically.
The controller php file customStatusController.php:
/**
* #Route("/custom-status", name="frontend.store.customplugin.custom-status", options={"seo"="false"}, methods={"GET"})
*/
public function index(): Response
return $this->renderStorefront('#Storefront/storefront/index.html.twig');
}
The view twig file index.html.twig:
{% sw_extends '#Storefront/storefront/page/content/index.html.twig' %}
{% block base_header_flyout %}
{{ dump(page) }}
{% endblock %}
How can I pass/inherit the page object that comes from the cms with all the child objects?
You need to inject theShopware\Storefront\Page\GenericPageLoader and then create a NavigationPage from it.
So something like should do the trick ( did not test it it ):
<?php
/**
* #var GenericPageLoader
*/
private $genericPageLoader;
....
public function __construct(GenericPageLoader $genericPageLoader)
{
$this->genericPageLoader = $genericPageLoader;
}
/**
* #Route("/custom-status", name="frontend.store.customplugin.custom-status", options={"seo"="false"}, methods={"GET"})
*/
public function index(Request $request, SalesChannelContext $context): Response
$page = $this->genericPageLoader->load($request, $context);
$page = NavigationPage::createFrom($page);
return $this->renderStorefront('#Storefront/storefront/index.html.twig',
[
'page' => $page,
]);
}
Then you should have at least the HeaderPagelet, FooterPagelet etc. within the $page variable.
You can also take a look at our Blog Plugin how we created the detail page with a custom Controller.
However: You also should ask yourself if a custom Controller is really needed, or if a CmsDataResolver / CMS Element would do the trick also which is way more flexible.
Related
I have two entities, Offer and Candidate, with a OneToMany relationship between them. The relevant code parts are the followings:
In Offer.php:
/**
* #ORM\OneToMany(targetEntity=Candidate::class, mappedBy="offer", orphanRemoval=true)
*/
private $candidates;
public function __construct()
{
$this->candidates = new ArrayCollection();
}
In Candidate.php:
/**
* #ORM\ManyToOne(targetEntity=Offer::class, inversedBy="candidates")
* #ORM\JoinColumn(nullable=false)
*/
private $offer;
In OfferCrudController.php:
public function configureFields(string $pageName): iterable
{
return [
ArrayField::new('candidates', new TranslatableMessage('easyadmin.candidates'))
->onlyOnDetail(),
AssociationField::new('candidates', new TranslatableMessage('easyadmin.candidates'))
->onlyOnIndex()
];
}
In CandidateCrudController.php:
public function configureFields(string $pageName): iterable
{
return [
AssociationField::new('offer', new TranslatableMessage('easyadmin.candidate.offer'))
];
}
My problem is that while in the case of Candidates EasyAdmin displays the Offer linked to its details page for each Candidate, for the Offers it displays only the non-clickable string representations of the Candidates, as the images show it:
Is it possible to display the Candidates too with their appropriate links to their details page?
Yes, it's possible, but you should create a custom template for this.
Let's say, you've created /templates/admin/field/offer/detail/candidates.html.twig. Then in OfferCrudController need to set created template for candidates field*:
public function configureFields(string $pageName): iterable
{
// ...
yield AssociationField::new('candidates', 'Candidates')->onlyOnDetail()
->setTemplatePath('admin/field/offer/detail/candidates.html.twig');
}
example with generator, but it can be easily written as array item, like in your example (instead ArrayField). I just prefer to use generator in that case, because that's more convenient to configure.
And in template just generate URL for each entity:
# /templates/admin/field/offer/detail/candidates.html.twig
{% if field.value is not empty %}
{% for candidate in field.value %}
{% set candidateDetailUrl = ea_url()
.setController('App\\Controller\\Admin\\CandidateCrudController')
.setAction(constant('EasyCorp\\Bundle\\EasyAdminBundle\\Config\\Action::DETAIL'))
.setEntityId(candidate.id)
%}
<a href="{{ candidateDetailUrl }}" style="display: block">
{{ candidate.name }}
</a>
{% endfor %}
{% else %}
No candidates
{% endif %}
What I would like to achieve is to output some dynamic text coming from DB filled with unpredictable number of placeholders to be filled with some query parameters.
Basically it is an automation/notification system whereby upon user or admin's interaction with the website, some automation tasks will get triggered and added to DB. My closest shot at almost handling it is by using twig |replace filter in connection with twig extension. The problem is that I get to see the replaced text with the raw data not their parsed value. I guess it's better to look at my code. Your help is greatly appreciated.
DB Schema 'AutomationMsgTemplate, aka: amt'
raw_msg(text type) | format_keys(text type) | format_values(text type)
You are %USERNAME% | USERNAME | row.user.username
%No% %ORD_STATS% created | NO, ORD_STATUS | row.notif.x, row.order.y
My Service 'MsgFormatHelper' (facilitating twig ext)
public function renderMsgUsingSprintFormat(AutomationMsgTemplate $amt, GeneratedAutomationTask $gat): array
{
$formatter_keys = $amt->getFormatKeys();
$formatter_values = $amt->getFormatValues();
$formatter_keys_arr = explode(",", $formatter_keys);
$formatter_values_arr = explode(",", $formatter_values);
$formatter_ready = array_combine($formatter_keys_arr, $formatter_values_arr);
return $formatter_ready;
}
Twig extension
class AppExtension extends AbstractExtension implements ServiceSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFunctions(): array
{
return [
new TwigFunction('msgSprint', [$this, 'renderMsg'])
];
}
public function renderMsg($amt, $gat): array
{
return $this->container
->get(MsgFormatHelper::class)
->renderMsgUsingSprintFormat($amt, $gat);
}
public static function getSubscribedServices(): array
{
return [
MsgFormatHelper::class
];
}
}
And finally inside the twig
{% for i, row in incompleteTasks %}
{% for amt in row.aet.automationMsgTemplates %}
// MOMENT OF TRUTH IS BELOW:
{% set format_arr = msgSprint(amt, row) %}
{% set processedMsg = amt.message|replace(format_arr) %}
{{ processedMsg }}
//nope, output is like: You are row.user.username
//sure enough below, as a debug test, works as intended
{# {% set processedMsg = temp.message|replace({'%USERNAME%': row.ats.studentCourse.student.username}) %} #}
{% endfor %}
{% endfor %}
I have my entity Article and one single table inheritance like this :
/**
* Article
*
* #ORM\Table(name="article")
* #ORM\Entity(repositoryClass="PM\PlatformBundle\Repository\ArticleRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="media", type="string")
* #ORM\DiscriminatorMap({"article" = "Article", "movie" = "Movie", "image" = "Image", "text" = "Text"})
*/
class Article
{
protected $id;
protected $title;
protected $description;
protected $author;
//other attributes and setters getters
}
class Image extends Article
{
private $path;
//getter setter
}
class Movie extends Article
{
private $url;
//getter setter
}
So my article's object type is either Image or movie or text only. Ok now I would like build a form wherein users can post a new article : in this form, the user has to choice between tree type (3 radios button) : image OR movie OR text only and of course the other fields : title and description. How I can do that ? Because with the command
php bin/console doctrine:generate:form myBundle:Article
The form rendered is :
class ArticleType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class)
->add('description', TextareaType::class)
->add('save', SubmitType::class);
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'PM\PlatformBundle\Entity\Article'
));
}
}
I don't know the way to implement my STI relation in this form. Because I have not field in my Article entity/object for the type (only in my table).
I have to add a Custom ChoiceType() field but it require a attribute.
When I try to add this in the form :
->add('path', SearchType::class)
->add('url', UrlType::class)
I got this error :
Neither the property "path" nor one of the methods "getPath()", "path()", "isPath()", "hasPath()", "__get()" exist and have public access in class "PM\PlatformBundle\Entity\Article".
Because I have create an instance of Article, not an instance of Image or Movie. Initially I created a STI thinking a new instance of Article would allow me also to define the "type" of article. But not ? Right ?
You will have to make three forms (one for an Article, one for a Movie and one for an Image). Then, in your controller, you have to options to deal with them:
Either you use one action to handle the three forms (you can check wich one is submitted by using $form->isSubmitted())
You create one action by form, and set the form action URL for each form to the correct controller.
Finally, in your template, you encapsulate your forms in a div, and use the example in my previous post.
{% extends "CoreBundle::layout.html.twig" %}
{% block title %}{{ parent() }}{% endblock %}
{% block btn_scrollspy %}
{% endblock %}
{% block bundle_body %}
<div class="well">
<div class="selector">
<input type="radio" name="form-selector" value="article-form"> Article
<input type="radio" name="form-selector" value="movie-form"> Movie
<input type="radio" name="form-selector" value="image-form"> Image
</div>
<div class="form article-form" style="display: none;">
{{ form(articleForm) }}
</div>
<div class="form movie-form" style="display: none;">
{{ form(movieForm) }}
</div>
<div class="form image-form" style="display: none;">
{{ form(imageForm) }}
</div>
</div>
{% endblock %}
I agree with dragoste in the comments: you can't expect the form to deduce by himself the type of class you want to instantiate based on a value.
Roughly, Image and Movie are the same type as Article, but an Article is not an Image and/or a Movie.
You will have to check that manually. You can do that server side like explained in the comments, with a field using mapped: false to determine the type of entity you need to instantiate, or client side with javascript by using three forms (one for a movie, one for an article, one for an image) and by displaying the correct one based on your radio button.
Edit: How to display the correct form in JS?
I created a JSFiddle to show you how you can do this using jQuery : https://jsfiddle.net/61gc6v16/
With jQuery documentation, you should be able to quickly understand what this sample do, and to adapt it to your needs. :)
#Boulzy I did this and it works:
class ArticleController extends Controller
{
public function addAction(Request $request)
{
$form1 = $this->get('form.factory')->create(TextOnlyType::class);
$form2 = $this->get('form.factory')->create(ImageType::class);
$form3 = $this->get('form.factory')->create(MovieType::class);
return $this->render('PMPlatformBundle:Article:add.html.twig', array(
'ArticleTextform' => $form1->createView(),
'ArticleImageform' => $form2->createView(),
'ArticleMovieform' => $form3->createView(),
));
}
public function addArticleTextAction(Request $request)
{
$ArticleText = new TextOnly;
$form = $this->get('form.factory')->create(TextOnlyType::class, $ArticleText);
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid()) {
$ArticleText->setAuthor(5);
$ArticleText->setLikes(0);
$em = $this->getDoctrine()->getManager();
$em->persist($ArticleText);
$em->flush();
$request->getSession()->getFlashBag()->add('notice', 'Annonce bien enregistrée.');
$listComments = $ArticleText->getComments();
return $this->render('PMPlatformBundle:Article:view.html.twig', array(
'article' => $ArticleText,
'listComments' => $listComments
));
}
return $this->render('PMPlatformBundle:Article:add.html.twig', array(
'form' => $form->createView(),
));
}
public function addArticleImageAction(Request $request)
{
//the same system as TextOnly
}
public function addArticleMovieAction(Request $request)
{
//the same system as TextOnly
}
}
(I override action directly in my template. With POST method.)
addAction is my contoller which is called by route for display the view of three forms. As you can see this code works as long as there is no error on submitted form. Because, in this case (when error occured when a form is submitted) each controller needs to return the initial view with the 3 forms. How I can do that ?
in a symfony2 application a entity Message has a one-to-many relation to documents. Documents represents user uploads. i created a form. I realized two Forms: MessageForm and DocumentForm. DocumentForm lives inside a collection FormField in MessageForm. Uploading and processing files does work.
But if i want to edit the entity Message the Form contains as many empty FileInputs as there are Documents existing. desired behaviour would be:
FileInputs to upload new files
Filename (link) to existing files
Possibility to delete existing files
This should be handled inside the form. Changes should be done when the form is submitted.
How can this be realized?
Solution is to write a custom form type extension. as described on http://symfony.com/doc/2.1/cookbook/form/create_form_type_extension.html.
filetype extension
<?php
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* Class FileTypeExtension
*
* #see http://symfony.com/doc/2.1/cookbook/form/create_form_type_extension.html
*/
class FileTypeExtension extends AbstractTypeExtension
{
/**
* Returns the name of the type being extended.
*
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return 'file';
}
/**
* Add the image_path option
*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('file_path', 'file_name'));
}
/**
* Pass the image url to the view
*
* #param FormView $view
* #param FormInterface $form
* #param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('file_path', $options)) {
$parentData = $form->getParent()->getData();
if (null !== $parentData) {
$propertyPath = new PropertyPath($options['file_path']);
$fileUrl = $propertyPath->getValue($parentData);
} else {
$fileUrl = null;
}
$view->set('file_url', $fileUrl);
}
if (array_key_exists('file_name', $options)) {
$parentData = $form->getParent()->getData();
if (null !== $parentData) {
$propertyPath = new PropertyPath($options['file_name']);
$fileName = $propertyPath->getValue($parentData);
} else {
$fileName = null;
}
$view->set('file_name', $fileName);
}
}
}
customized file_widget
{% block file_widget %}
{% spaceless %}
{% if file_url is not null %}
<div>{{ file_name }}</div>
<div style="display:none">{{ block('form_widget') }}</div>
{% else %}
{{ block('form_widget') }}
{% endif %}
{% endspaceless %}
{% endblock %}
services.yml
parameters:
foobar.file_type_extension.class: Foobar\Form\Extension\FileTypeExtension
services:
foobar.file_type_extension:
class: %replacethis.file_type_extension.class%
tags:
- { name: form.type_extension, alias: file }
inside a formtype
$builder->add('file','file', array(
"label" => "Datei",
"required" => true,
"attr" => array(),
"file_path" => "webPath",
"file_name" => "name"
));
that's it ;)
In addition to the above answere - wich will let you build a file input wich can be rendered as a link (if it has an url) or a field (if it doesn't) - you might take a look at
http://symfony.com/doc/2.0/cookbook/form/form_collections.html
Wich in conjunction with some jQuery will let you add fields in UI.
This is the code that gets executed (as displayed in "code behind this page" section):
Controller Code
/**
* #Route("/hello/{name}", name="_demo_hello")
* #Template()
*/
public function helloAction($name)
{
$name = "whatever";
$this->render('AcmeDemoBundle:Demo:hello.html.twig',
array('name' => '123'));
//return array('name' => 'abc');
}
Template Code
{% extends "AcmeDemoBundle::layout.html.twig" %}
{% block title "Hello " ~ name %}
{% block content %}
<h1>xHello {{ name }}!</h1>
{% endblock %}
The output is
xHello Raffael!
The URL: http://192.168.177.128/Symfony/web/app_dev.php/demo/hello/Raffael
And here is my problem:
When I uncomment the return within controller then "Raffael" is replaced with "abc" as expected.
But according to the Quicktour it is possible to determine the values of variables within the template via the render-Method.
To render a template in Symfony, use the render method from within a
controller and pass it any variables needed in the template:
$this->render('AcmeDemoBundle:Demo:hello.html.twig', array( 'name' =>
$name, ));
What's wrong?
Further down the quick tour implicitely reveales that you have to return the output of render:
public function helloAction($name)
{
return $this->render('AcmeDemoBundle:Demo:hello.html.twig',
array('name' => '123'));
}