Image upload with sonata admin - symfony

See EDIT above
I think the issue is pretty simple to solve but I can't find any clear answer right now. I hope you might have an idea.
I'm trying to upload an image with sonata admin.
In my entity I have this field
/**
* #ORM\Column(type="string", length=2000)
* #Assert\File(mimeTypes={ "image/png", "image/jpeg" })
*/
private $image;
When I go to the sonata admin form view. The button Upload file is there and defined as below
$formMapper->add('image', FileType::class);
But when I try to send the form, I'm getting this error
The form's view data is expected to be an instance of class Symfony\Component\HttpFoundation\File\File, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\Component\HttpFoundation\File\File.
I hint this is due to the doctrine string type. But I don't think doctrine has a "File" type.
Thanks for your help.
EDIT:
Considering the link provided in comment, here is the new error
The current field image is not linked to an admin. Please create one for the target entity : ``
<?php
namespace App\Entity;
// src/Entity/Image.php
class Image{
const SERVER_PATH_TO_IMAGE_FOLDER = '/public/images';
/**
* Unmapped property to handle file uploads
*/
private $file;
/**
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Manages the copying of the file to the relevant place on the server
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// we use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and target filename as params
$this->getFile()->move(
self::SERVER_PATH_TO_IMAGE_FOLDER,
$this->getFile()->getClientOriginalName()
);
// set the path property to the filename where you've saved the file
$this->filename = $this->getFile()->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->setFile(null);
}
/**
* Lifecycle callback to upload the file to the server.
*/
public function lifecycleFileUpload()
{
$this->upload();
}
/**
* Updates the hash value to force the preUpdate and postUpdate events to fire.
*/
public function refreshUpdated()
{
$this->setUpdated(new \DateTime());
}
// ... the rest of your class lives under here, including the generated fields
// such as filename and updated
}
In my ForumAdmin, now I have
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('name', TextType::class);
$formMapper->add('description', TextAreaType::class);
$formMapper->add('weight', IntegerType::class);
$formMapper->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
]);
$formMapper
->add('image', AdminType::class)
;
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('name');
$datagridMapper->add('category');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('name');
$listMapper->addIdentifier('description');
$listMapper->addIdentifier('category');
$listMapper->addIdentifier('weight');
$listMapper->addIdentifier('createdAt');
$listMapper->addIdentifier('updatedAt');
$listMapper->addIdentifier('image');
}
public function prePersist($object)
{
parent::prePersist($object);
$this->manageEmbeddedImageAdmins($object);
if($object instanceof Forum){
$object->setCreatedAt(new \DateTime('now'));
$object->setUpdatedAt(new \DateTime('now'));
$object->setStatus("NO_NEW");
}
}
public function preUpdate($page)
{
$this->manageEmbeddedImageAdmins($page);
}
private function manageEmbeddedImageAdmins($page)
{
// Cycle through each field
foreach ($this->getFormFieldDescriptions() as $fieldName => $fieldDescription) {
// detect embedded Admins that manage Images
if ($fieldDescription->getType() === 'sonata_type_admin' &&
($associationMapping = $fieldDescription->getAssociationMapping()) &&
$associationMapping['targetEntity'] === 'App\Entity\Image'
) {
$getter = 'get'.$fieldName;
$setter = 'set'.$fieldName;
/** #var Image $image */
$image = $page->$getter();
if ($image) {
if ($image->getFile()) {
// update the Image to trigger file management
$image->refreshUpdated();
} elseif (!$image->getFile() && !$image->getFilename()) {
// prevent Sf/Sonata trying to create and persist an empty Image
$page->$setter(null);
}
}
}
}
}
I also have this ImageAdmin even if I don't see why it would be usefull
final class ImageAdmin extends AbstractAdmin{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('image', FileType::class);
}
public function prePersist($image)
{
$this->manageFileUpload($image);
}
public function preUpdate($image)
{
$this->manageFileUpload($image);
}
private function manageFileUpload($image)
{
if ($image->getFile()) {
$image->refreshUpdated();
}
}
// ...
}

The current field image is not linked to an admin. Please create one
for the target entity : ``
To fix this error you need add the service in file sonata_admin.yaml like this:
services:
...
admin.image:
class: App\Admin\ImageAdmin
arguments: [~, App\Entity\File, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: 'admin.image' }
public: true

Related

How to toggle Symfony's php-translation/symfony-bundle EditInPlace

I followed this documentation for Edit In Place, and setup the Activator, and it works!
However, I will be using this on the production site and allowing access via a ROLE_TRANSLATOR Authorization. This is also working, but I don't want the web interface always "on"
How would I go about enabling it via some sort of link or toggle?
My thoughts, it would be simple to just add a URL parameter, like ?trans=yes and then in the activator;
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $_GET['trans'] == 'yes');
Obviously, $_GET would not work, I didn't even try.
How do I generate a link to simply reload THIS page with the extra URL parameter
How do I check for that parameter within the "Activator"
or, is there a better way?
The "proper" way to do this, as I have discovered more about "services" is to do the logic diectly in the RoleActivator.php file.
referencing documentation for How to Inject Variables into all Templates via Referencing Services I came up with the following solution;
src/Security/RoleActivator.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Translation\Bundle\EditInPlace\ActivatorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class RoleActivator implements ActivatorInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var TranslatorInterface
*/
private $translate;
/**
* #var RequestStack
*/
private $request;
private $params;
private $path;
private $flag = null;
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translate, RequestStack $request)
{
$this->authorizationChecker = $authorizationChecker;
$this->translate = $translate;
$this->request = $request;
}
/**
* {#inheritdoc}
*/
public function checkRequest(Request $request = null)
{
if ($this->flag === null) { $this->setFlag($request); }
try {
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $this->flag);
} catch (AuthenticationCredentialsNotFoundException $e) {
return false;
}
}
public function getText()
{
if ($this->flag === null) { $this->setFlag(); }
return ($this->flag) ? 'linkText.translate.finished' : 'linkText.translate.start'; // Translation key's returned
}
public function getHref()
{
if ($this->flag === null) { $this->setFlag(); }
$params = $this->params;
if ($this->flag) {
unset($params['trans']);
} else {
$params['trans'] = 'do';
}
$queryString = '';
if (!empty($params)) {
$queryString = '?';
foreach ($params as $key => $value) {
$queryString.= $key.'='.$value.'&';
}
$queryString = rtrim($queryString, '&');
}
return $this->path.$queryString;
}
private function setFlag(Request $request = null)
{
if ($request === null) {
$request = $this->request->getCurrentRequest();
}
$this->flag = $request->query->has('trans');
$this->params = $request->query->all();
$this->path = $request->getPathInfo();
}
}
config\packages\twig.yaml
twig:
# ...
globals:
EditInPlace: '#EditInPlace_RoleActivator'
config\services.yaml
services:
# ...
EditInPlace_RoleActivator:
class: App\Security\RoleActivator
arguments: ["#security.authorization_checker"]
So What I added over and above the php-translation example is the getText and getHref methods and corresponding private variables being set in the checkRequest and read there after.
Now in my twig template (in the header) I just use
{% if is_granted('ROLE_TRANSLATOR') %}
{{ EditInPlace.Text }}
{% endif %}
Add the new keys to the translation files and your done. the trans=do query parameter is toggled on and off with each click of the link. You could even add toggling styles with a class name, just copy the getText method to something like getClass and return string a or b with the Ternary.

Upload multiple files for each entity

I am using doctrine2 with symfony2.
This is my entity to upload the file.
First, it call the setFile() and put the path to $this->temp,
then,preUpload is called ,upload called.
It is OK for uploading onefile for each entity,however, I would like to upload multiple files for each entity.
How can I handle this ?
Do you have any samples for this purpose?
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $path = "nophoto.jpeg";
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
// check if we have an old image path
if (is_file($this->getAbsolutePath())) {
// store the old name to delete after the update
$this->temp = $this->getAbsolutePath();
} else {
$this->path = 'initial';
}
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
$this->path = $this->getId().'.'.$this->getFile()->guessExtension();
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->getFile1()) {return;}
if (isset($this->temp)) {
// delete the old image
unlink($this->temp);
// clear the temp image path
$this->temp = null;
}
// you must throw an exception here if the file cannot be moved
// so that the entity is not persisted to the database
// which the UploadedFile move() method does
$this->getFile()->move(
$this->getUploadRootDir(),
$this->getId().'.'.$this->getFile()->guessExtension()
);
$this->setFile(null);
}
public function getAbsolutePath()
{
return null === $this->path
? null
: $this->getUploadRootDir().'/'.$this->getId().'.'.$this->path;
}
public function getFile1()
{
return $this->file;
}
public function getWebPath()
{
return null === $this->path
? null
: $this->getUploadDir().'/'.$this->path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded
// documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw up
// when displaying uploaded doc/image in the view.
return 'uploads/documents';
}
You need new entity which will represent uploaded file with many-to-one (or many-to-many) association to your entity. This is most universal approach.
Alternatively you can store file names in array but that will complicate your validation and forms.

JMS Serializer: Dynamically change the name of a virtual property at run time

I use JMS Serializer Bundle and Symfony2. I am using VirtualProperties. currently, I set the name of a property using the SerializedName annotation.
/**
* #JMS\VirtualProperty()
* #JMS\SerializedName("SOME_NAME")
*/
public function getSomething()
{
return $this->something
}
Is it possible to set the serialized name dynamically inside the function? Or is it possible to dynamically influence the name using Post/Pre serialization events?
Thanks!
I don't think you can do this directly, but you could accomplish something similar by having several virtual properties, one for each possible name. If the name is not relevant to a particular entity, have the method return null, and disable null serialization in the JMS config.
In the moment when you go to serialize the object, do the following:
$this->serializer = SerializerBuilder::create()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())->build();
$json = $this->serializer->serialize($object, 'json');
dump($json);
Entity
/**
* #JMS\VirtualProperty("something", exp="context", options={
* #JMS\Expose,
* })
*/
class SomeEntity
{
}
Event Listener
abstract class AbstractEntitySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
[
'event' => Events::POST_SERIALIZE,
'method' => 'onPostSerialize',
'class' => static::getClassName(),
'format' => JsonEncoder::FORMAT,
'priority' => 0,
],
];
}
public function onPostSerialize(ObjectEvent $event): void
{
foreach ($this->getMethodNames() as $methodName) {
$visitor = $event->getVisitor();
$metadata = new VirtualPropertyMetadata(static::getClassName(), $methodName);
if ($visitor->hasData($metadata->name)) {
$value = $this->{$methodName}($event->getObject());
$visitor->visitProperty(
new StaticPropertyMetadata(static::getClassName(), $metadata->name, $value),
$value
);
}
}
}
abstract protected static function getClassName(): string;
abstract protected function getMethodNames(): array;
}
...
class SomeEntitySubscriber extends AbstractEntitySubscriber
{
protected static function getClassName(): string
{
return SomeEntity::class;
}
protected function getMethodNames(): array
{
return ['getSomething'];
}
protected function getSomething(SomeEntity $someEntity)
{
return 'some text';
}
}

Symfony2 - Setting up tags for a blog

New to Symfony2, can someone give me some advice on how to setup tags for a blog site? I've setup tags as it's own entity and will relate tags to the blog entity via ManyToMany relation.
My question is how would I set this up in twig?
In other words, I have a form to entering a new blog, do I setup a new form for just entering tags? Or is there a way to combine entering tags with the blog creation form?
Tags are just a list of unique strings.
Front : I use Select2, there is a really good tag feature : http://ivaynberg.github.io/select2/. It will take/return a string to the server with each "tag" separated by a comma : tag1,tag2,tag3. You can also configure a web-service to research existing tags.
Back : I create a DataTransformer (http://symfony.com/doc/current/cookbook/form/data_transformers.html) as a service and i inject the entity manager in it :
class TagsTransformer implements DataTransformerInterface
{
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function transform($value)
{
if (null === $value) {
return '';
}
if ($value instanceof Collection) {
return implode(',', array_map(function (Tag $tag) {
return (string)$tag;
}, $value->toArray()));
}
return $value;
}
public function reverseTransform($value)
{
if (empty($value)) {
return null;
}
if (is_string($value)) {
$values = explode(',', $value);
foreach ($values as &$value) {
// Find or create it (create the method)
$value = $this->em->getRepository('MySuperBundle:Tag')->findOrCreate(trim($value));
}
unset($value);
return $values;
}
return $value;
}
}
The goal of this transformer is to :
Transform : take the ArrayCollection of Tags entity from Doctrine, and convert it as a simple comma separated string
Reverse : take a simple comma separated string and convert it as an array of unique Tag entity
We then create a Form for tags (again, as a service, with the data transformer in it) :
class TagsType extends AbstractType
{
private $tagsTransformer;
/**
* #param TagsTransformer $tagsTransformer
*/
public function __construct(TagsTransformer $tagsTransformer)
{
$this->tagsTransformer = $tagsTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer($this->tagsTransformer);
}
public function getParent()
{
return 'text';
}
}
Finally, in your form (blog post form for example), you'll have to use your TagsType new "field".

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources