Can you have multiple instantiations of one Blade X component?
// app/view/components/foo.php
class Foo extends Component {
public $name;
public function __construct($name)
{
$this->name = $name;
}
...
// resources/views/components/foo.blade.php
<div>
{{ $name }}
</div>
// resources/views/rootcomponent.blade.php
<x-foo name="John"></x-foo>
<x-foo name="Mary"></x-foo>
It says:
Cannot declare class App\View\Components\Foo, because the name is already in use
If you use it once - or use it without constructor / class variables, it works.
Maybe I'll go with the anonymous way, I was just curious what am I doing wrong.
It seems that base class already has that property, so it doesn't need to be redeclared.
// app/view/components/foo.php
class Foo extends Component {
/* doesn't need to redeclare base class property */
//public $name;
public function __construct($name)
{
parent::__construct($name);
/* doesn't need this line anymore. */
// $this->name = $name;
/* < Custom command line here.
... >
But if there is no custom command line needed,
then this constructor is useless,
so no need to declare this.
*/
}
...
Hope this could helps anyone.
Related
I use DTOs as the data_class for Symfony form types. There is one thing that does not work for me when I use typed properties (PHP 7.4) in these DTOs.
EXAMPLE:
class ProductDto
{
/*
* #Assert\NotBlank
*/
public string $title;
}
This generally seems to work quite well – in case the user submits the form with a blank title or description, the validation kicks in and the form is displayed with validation warnings.
BUT THERE IS A PROBLEM when data is added while creating a form (e.g. the edit form):
$productDto = new ProductDto();
$productDto->title = 'Foo';
$form = $this->createForm(ProductFormType::class, $productDto);
Initially the form is displayed as expected with Foo as the value for the title. When a user clears the title input form field and submits the form an exception like this is thrown:
Typed property `App\Form\Dto\ProductDto::$title` must be string, null used
As far as I can see this is caused by the fact that during Form->handleRequest() the title is set to null after it was set to "Foo" before, right?
Is there a solution for this problem?
Since PHP 7.4 introduces type-hinting for properties, it is particularly important to provide valid values for all properties, so that all properties have values that match their declared types.
A property that has never been assigned doesn't have a null value, but it is on an undefined state, which will never match any declared type. undefined !== null.
Here is an example:
<?php
class Foo
{
private int $id;
private ?string $val;
public function __construct(int $id)
{
$this->id = $id;
}
}
For the code above, if you did:
<?php
foo = new Foo(1);
$foo->getVal();
You would get:
Fatal error: Uncaught Error: Typed property Foo::$val must not be
accessed before initialization
See this post for more details https://stackoverflow.com/a/59265626/3794075
and see this bug https://bugs.php.net/bug.php?id=79620
This is what I just came up with:
DTO:
use GenericSetterTrait;
/**
* #Assert\NotBlank
*/
public string $title;
public function setTitle(?string $title): void
{
$this->set('title', $title);
}
/**
* #Assert\NotNull
*/
public Foo $foo;
public function setFoo(?Foo $foo): void
{
$this->set('foo', $foo);
}
Trait:
trait GenericSetterTrait
{
private function set(string $propertyName, $value): void
{
if ($value === null) {
unset($this->{$propertyName});
} else {
$this->{$propertyName} = $value;
}
}
}
Seems to work. What do you think? Any objections?
I'm trying to validate my entity via static callback.
I was able to make it work following the Symfony guide but something isn't clear to me.
public static function validate($object, ExecutionContextInterface $context, $payload)
{
// somehow you have an array of "fake names"
$fakeNames = array(/* ... */);
// check if the name is actually a fake name
if (in_array($object->getFirstName(), $fakeNames)) {
$context->buildViolation('This name sounds totally fake!')
->atPath('firstName')
->addViolation()
;
}
}
It works fine when I populate my $fakeNames array but what if I want to make it "dynamic"? Let's say I want to pick that array from the parameters or from the database or wherever.
How am I supposed to pass stuff (eg. the container or entityManager) to this class from the moment that the constructor doesn't work and it has to be necessarily static?
Of course my approach may be completely wrong but I'm just using the symfony example and few other similar issues found on the internet that I'm trying to adapt to my case.
You can create a Constraint and Validator and register it as service so you can inject entityManager or anything you need, you can read more here:
https://symfony.com/doc/2.8/validation/custom_constraint.html
or if you are on symfony 3.3 it is already a service and you can just typehint it in your constructor:
https://symfony.com/doc/current/validation/custom_constraint.html
This is the solution I was able to find in the end.
It works smoothly and I hope it may be useful for someone else.
I've set the constraint on my validation.yml
User\UserBundle\Entity\Group:
constraints:
- User\UserBundle\Validator\Constraints\Roles\RolesConstraint: ~
Here is my RolesConstraint class
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\Validator\Constraint;
class RolesConstraint extends Constraint
{
/** #var string $message */
public $message = 'The role "{{ role }}" is not recognised.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and here is my RolesConstraintValidator class
<?php
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RolesConstraintValidator extends ConstraintValidator
{
/** #var ContainerInterface */
private $containerInterface;
/**
* #param ContainerInterface $containerInterface
*/
public function __construct(ContainerInterface $containerInterface)
{
$this->containerInterface = $containerInterface;
}
/**
* #param \User\UserBundle\Entity\Group $object
* #param Constraint $constraint
*/
public function validate($object, Constraint $constraint)
{
if (!in_array($object->getRole(), $this->containerInterface->getParameter('roles'))) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ role }}', $object->getRole())
->addViolation();
}
}
}
Essentially, I set up a constraint which, every time a new user user is registered along with the role, that role must be among those set in the parameters. If not, it builds a violation.
Is there a way to add own helpers to the console HelperSet in Symfony3?
I didn't find any helpful thing in the documentation.
Ok i followed the code and find a simple solution. :)
I just have to add my class that implements the HelperInterface, or extend the abstract Helper class.
$this->getHelperSet()->set(new MyHelper(), 'myhelper');
And myhelper class looks like that:
<?php
namespace MyApp\Helper;
use Symfony\Component\Console\Helper\Helper;
class MyHelper extends Helper
{
/**
* #param $string
* #return string
*/
public function doIt($string) {
return 'this is your '.$string;
}
/**
* Returns the canonical name of this helper.
*
* #return string The canonical name
*/
public function getName() {
return 'myhelper';
}
}
And in my code i can use it like:
$myhelper = $this->helperSet->get('myhelper');
$myString = $myhelper->doIt('hallo');
:)
Say I have a FormType in a re-usable bundle like so:
/**
* #Service("acme_core_purchase.form.add_to_basket")
* #FormType
*/
class AddToBasketType extends AbstractType
{}
In my controller I inject it, to do whatever I need:
/** #Inject("acme_core_purchase.form.add_to_basket") */
protected $add_to_basket_form;
Say in a particular project I want to extend that form and add some extra fields in, how am I meant to do this with annotations?
class AddToBasketType extends BaseAddToBasketType
{}
I can't just create a form with the same service name, that doesn't
work / make sense- the assignment doesn't follow the priority set in
AppKernel::registerBundles as far as I can tell. I suspect this is
conceptually flawed anyway.
I can change the service name, but then I need to inject a different
service everywhere I've used it, which involves extending all of
those things.
I can store the service name in a parameter then inject that
everywhere instead, that way I can have the correct service name
injected everywhere, but I'd also need to inject the container to be
able to retrieve the service.
I've been doing the latter wherever I've needed to, but surely there
is a better way? Or at least, there should be.
Simple, use the Form component own inheritance.
/**
* #Service("acme_core_purchase.form.add_to_basket")
* #FormType
*/
class AddToBasketType extends AbstractType
{
public function getName() { return 'add_to_basket'; }
public function getParent() { return 'form'; }
}
/**
* #Service("whatever")
* #FormType
*/
class AddToBasket2Type extends AbstractType
{
public function getName() { return 'add_to_basket2'; }
public function getParent() { return 'add_to_basket'; }
}
Take a look at the service piece of their annotation configs
Notice the parent="another.service.id"
<?php
use JMS\DiExtraBundle\Annotation\Service;
/**
* #Service("some.service.id", parent="another.service.id", public=false)
*/
class Listener
{
}
I have a loop in Twig template, which returns multiple values. Most important - an ID of my entry. When I didn't use any framework nor template engine, I used simply file_exists() within the loop. Now, I can't seem to find a way to do it in Twig.
When I display user's avatar in header, I use file_exists() in controller, but I do it because I don't have a loop.
I tried defined in Twig, but it doesn't help me. Any ideas?
If you want want to check the existence of a file which is not a twig template (so defined can't work), create a TwigExtension service and add file_exists() function to twig:
src/AppBundle/Twig/Extension/TwigExtension.php
<?php
namespace AppBundle\Twig\Extension;
class FileExtension extends \Twig_Extension
{
/**
* Return the functions registered as twig extensions
*
* #return array
*/
public function getFunctions()
{
return array(
new Twig_SimpleFunction('file_exists', 'file_exists'),
);
}
public function getName()
{
return 'app_file';
}
}
?>
Register your service:
src/AppBundle/Resources/config/services.yml
# ...
parameters:
app.file.twig.extension.class: AppBundle\Twig\Extension\FileExtension
services:
app.file.twig.extension:
class: %app.file.twig.extension.class%
tags:
- { name: twig.extension }
That's it, now you are able to use file_exists() inside a twig template ;)
Some template.twig:
{% if file_exists('/home/sybio/www/website/picture.jpg') %}
The picture exists !
{% else %}
Nope, Chuck testa !
{% endif %}
EDIT to answer your comment:
To use file_exists(), you need to specify the absolute path of the file, so you need the web directory absolute path, to do this give access to the webpath in your twig templates
app/config/config.yml:
# ...
twig:
globals:
web_path: %web_path%
parameters:
web_path: %kernel.root_dir%/../web
Now you can get the full physical path to the file inside a twig template:
{# Display: /home/sybio/www/website/web/img/games/3.jpg #}
{{ web_path~asset('img/games/'~item.getGame.id~'.jpg') }}
So you'll be able to check if the file exists:
{% if file_exists(web_path~asset('img/games/'~item.getGame.id~'.jpg')) %}
I've created a Twig function which is an extension of the answers I have found on this topic. My asset_if function takes two parameters: the first one is the path for the asset to display. The second parameter is the fallback asset, if the first asset does not exist.
Create your extension file:
src/Showdates/FrontendBundle/Twig/Extension/ConditionalAssetExtension.php:
<?php
namespace Showdates\FrontendBundle\Twig\Extension;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ConditionalAssetExtension extends \Twig_Extension
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Returns a list of functions to add to the existing list.
*
* #return array An array of functions
*/
public function getFunctions()
{
return array(
'asset_if' => new \Twig_Function_Method($this, 'asset_if'),
);
}
/**
* Get the path to an asset. If it does not exist, return the path to the
* fallback path.
*
* #param string $path the path to the asset to display
* #param string $fallbackPath the path to the asset to return in case asset $path does not exist
* #return string path
*/
public function asset_if($path, $fallbackPath)
{
// Define the path to look for
$pathToCheck = realpath($this->container->get('kernel')->getRootDir() . '/../web/') . '/' . $path;
// If the path does not exist, return the fallback image
if (!file_exists($pathToCheck))
{
return $this->container->get('templating.helper.assets')->getUrl($fallbackPath);
}
// Return the real image
return $this->container->get('templating.helper.assets')->getUrl($path);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'asset_if';
}
}
Register your service (app/config/config.yml or src/App/YourBundle/Resources/services.yml):
services:
showdates.twig.asset_if_extension:
class: Showdates\FrontendBundle\Twig\Extension\ConditionalAssetExtension
arguments: ['#service_container']
tags:
- { name: twig.extension }
Now use it in your templates like this:
<img src="{{ asset_if('some/path/avatar_' ~ app.user.id, 'assets/default_avatar.png') }}" />
I've had the same problem as Tomek. I've used Sybio's solution and made the following changes:
app/config.yml => add "/" at the end of web_path
parameters:
web_path: %kernel.root_dir%/../web/
Call file_exists without "asset" :
{% if file_exists(web_path ~ 'img/games/'~item.getGame.id~'.jpg') %}
Hope this helps.
Here is my solution, using SF4, autowire and autoconfigure:
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Symfony\Component\Filesystem\Filesystem;
class FileExistsExtension extends AbstractExtension
{
private $fileSystem;
private $projectDir;
public function __construct(Filesystem $fileSystem, string $projectDir)
{
$this->fileSystem = $fileSystem;
$this->projectDir = $projectDir;
}
public function getFunctions(): array
{
return [
new TwigFunction('file_exists', [$this, 'fileExists']),
];
}
/**
* #param string An absolute or relative to public folder path
*
* #return bool True if file exists, false otherwise
*/
public function fileExists(string $path): bool
{
if (!$this->fileSystem->isAbsolutePath($path)) {
$path = "{$this->projectDir}/public/{$path}";
}
return $this->fileSystem->exists($path);
}
}
In services.yaml:
services:
App\Twig\FileExistsExtension:
$projectDir: '%kernel.project_dir%'
In templates:
# Absolute path
{% if file_exists('/tmp') %}
# Relative to public folder path
{% if file_exists('tmp') %}
I am new to Symfony so every comments are welcome!
Also, as initial question is about Symfony 2, maybe my answer is not relevant and I would better ask a new question and answer by myself?
Improving on Sybio's answer, Twig_simple_function did not exist for my version and nothing here works for external images for example. So my File extension file is like this:
namespace AppBundle\Twig\Extension;
class FileExtension extends \Twig_Extension
{
/**
* {#inheritdoc}
*/
public function getName()
{
return 'file';
}
public function getFunctions()
{
return array(
new \Twig_Function('checkUrl', array($this, 'checkUrl')),
);
}
public function checkUrl($url)
{
$headers=get_headers($url);
return stripos($headers[0], "200 OK")?true:false;
}
Just add a little comment to the contribution of Sybio:
The Twig_Function_Function class is deprecated since version 1.12 and
will be removed in 2.0. Use Twig_SimpleFunction instead.
We must change the class Twig_Function_Function by Twig_SimpleFunction:
<?php
namespace Gooandgoo\CoreBundle\Services\Extension;
class TwigExtension extends \Twig_Extension
{
/**
* Return the functions registered as twig extensions
*
* #return array
*/
public function getFunctions()
{
return array(
#'file_exists' => new \Twig_Function_Function('file_exists'), // Old class
'file_exists' => new \Twig_SimpleFunction('file_exists', 'file_exists'), // New class
);
}
public function getName()
{
return 'twig_extension';
}
}
The rest of code still works exactly as said Sybio.