I'm using Symfony 3.4 to work on a simple REST API microservice. There are not much resources to be found when working with HTTP APIs and file uploads. I'm following some of the instructions from the documentation but I found a wall.
What I want to do is to store the relative path to an uploaded file on an entity field, but it seems like the validation expects the field to be a full path.
Here's some of my code:
<?php
// BusinessClient.php
namespace DemoBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use ApiBundle\Entity\BaseEntity;
use ApiBundle\Entity\Client;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints;
/**
* Class BusinessClient
* #package DemoBundle\Entity
* #ORM\Entity(repositoryClass="DemoBundle\Repository\ClientRepository")
* #ORM\Table(name="business_client")
* #Serializer\ExclusionPolicy("all")
* #Serializer\AccessorOrder("alphabetical")
*/
class BusinessClient extends BaseEntity
{
/**
* #Constraints\NotBlank()
* #ORM\ManyToOne(targetEntity="ApiBundle\Entity\Client", fetch="EAGER")
* #ORM\JoinColumn(name="client_id", referencedColumnName="oauth2_client_id", nullable=false)
*/
public $client;
/**
* #Constraints\NotBlank()
* #ORM\Column(type="string", length=255, nullable=false)
* #Serializer\Expose
*/
protected $name;
/**
* #Constraints\Image(minWidth=100, minHeight=100)
* #ORM\Column(type="text", nullable=true)
*/
protected $logo;
/**
* One Business may have many brands
* #ORM\OneToMany(targetEntity="DemoBundle\Entity\Brand", mappedBy="business")
* #Serializer\Expose
*/
protected $brands;
/**
* BusinessClient constructor.
*/
public function __construct()
{
$this->brands = new ArrayCollection();
}
/**
* Set the links property for the resource response
*
* #Serializer\VirtualProperty(name="_links")
* #Serializer\SerializedName("_links")
*/
public function getLinks()
{
return [
"self" => "/clients/{$this->getId()}",
"brands" => "/clients/{$this->getId()}/brands"
];
}
/**
* Get the name of the business client
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the name of the business client
*
* #param string $name
*/
public function setName($name): void
{
$this->name = $name;
}
/**
* Get the logo
*
* #Serializer\Expose
* #Serializer\VirtualProperty(name="logo")
* #Serializer\SerializedName("logo")
*/
public function getLogo()
{
return $this->logo;
}
/**
* Set the logo field
*
* #param string|File|UploadedFile $logo
*/
public function setLogo($logo): void
{
$this->logo = $logo;
}
/**
* Get the client property
*
* #return Client
*/
public function getClient()
{
return $this->client;
}
/**
* Set the client property
*
* #param Client $client
*/
public function setClient($client): void
{
$this->client = $client;
}
}
Uploader Service:
<?php
namespace DemoBundle\Services;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Class FileUploader
* #package DemoBundle\Services
*/
class FileUploader
{
/** #var string $uploadDir The directory where the files will be uploaded */
private $uploadDir;
/**
* FileUploader constructor.
* #param $uploadDir
*/
public function __construct($uploadDir)
{
$this->uploadDir = $uploadDir;
}
/**
* Upload a file to the specified upload dir
* #param UploadedFile $file File to be uploaded
* #return string The unique filename generated
*/
public function upload(UploadedFile $file)
{
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move($this->getTargetDirectory(), $fileName);
return $fileName;
}
/**
* Get the base dir for the upload files
* #return string
*/
public function getTargetDirectory()
{
return $this->uploadDir;
}
}
I've registered the service:
services:
# ...
public: false
DemoBundle\Services\FileUploader:
arguments:
$uploadDir: '%logo_upload_dir%'
And last the controller:
<?php
namespace DemoBundle\Controller;
use ApiBundle\Exception\HttpException;
use DemoBundle\Entity\BusinessClient;
use DemoBundle\Services\FileUploader;
use FOS\RestBundle\Controller\Annotations as REST;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints\ImageValidator;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
* Class BusinessClientController
* #package DemoBundle\Controller
*/
class BusinessClientController extends BaseController
{
/**
* Create a new business entity and persist it in database
*
* #REST\Post("/clients")
* #SWG\Tag(name="business_clients")
* #SWG\Response(
* response="201",
* description="Create a business client and return it's data"
* )
* #param Request $request
* #param FileUploader $uploader
* #return Response
* #throws HttpException
*/
public function createAction(Request $request, FileUploader $uploader, LoggerInterface $logger)
{
$entityManager = $this->getDoctrine()->getManager();
$oauthClient = $this->getOauthClient();
$data = $request->request->all();
$client = new BusinessClient();
$client->setName($data["name"]);
$client->setClient($oauthClient);
$file = $request->files->get('logo');
if (!is_null($file)) {
$fileName = $uploader->upload($file);
$client->setLogo($fileName);
}
$errors = $this->validate($client);
if (count($errors) > 0) {
$err = [];
/** #var ConstraintViolationInterface $error */
foreach ($errors as $error) {
$err[] = [
"message" => $error->getMessage(),
"value" => $error->getInvalidValue(),
"params" => $error->getParameters()
];
}
throw HttpException::badRequest($err);
}
$entityManager->persist($client);
$entityManager->flush();
$r = new Response();
$r->setContent($this->serialize($client));
$r->setStatusCode(201);
$r->headers->set('Content-type', 'application/json');
return $r;
}
/**
* Get data for a single business client
*
* #REST\Get("/clients/{id}", requirements={"id" = "\d+"})
* #param $id
* #return Response
* #SWG\Tag(name="business_clients")
* #SWG\Response(
* response="200",
* description="Get data for a single business client"
* )
*/
public function getClientAction($id)
{
$object = $this->getDoctrine()->getRepository(BusinessClient::class)
->find($id);
$j = new Response($this->serialize($object));
return $j;
}
}
When I try to set the logo as a file basename string the request will fail. with error that the file (basename) is not found. This makes sense in a way.
If otherwise I try to set not a string but a File with valid path to the newly uploaded file the request will succeed, but the field in the table will be replaced with a full system path. The same happens when I put a valid system path instead of a file.
<?php
// Controller
.....
// This works
if (!is_null($file)) {
$fileName = $uploader->upload($file);
$client->setLogo($this->getParameter("logo_upload_dir")."/$fileName");
}
Parameter for the upload dir:
parameters:
logo_upload_dir: '%kernel.project_dir%/web/uploads/logos'
I'm not using any forms as this is a third party API and I'm mainly using the request objects directly to handle the data. Most of the documentations used Forms to handle this. Also all my responses are in JSON.
I'd appreciate any help on this. Otherwise I'll have to store the full path and that in not a good idea and very impractical.
Thanks in advance.
Here is a thought on this: Instead of validating the property which your plan to be a relative path to an image, validate the method. Something like this maybe:
class BusinessClient extends BaseEntity
{
public static $basePath;
// ....
/**
* Get the logo
*
* #Constraints\Image(minWidth=100, minHeight=100)
*/
public function getAbsolutePathLogo()
{
return self::$basePath . '/' . $this->logo;
}
So, remove the validation from your logo member, add a new method (I named it getAbsolutePathLogo buy you can choose anything) and set up validation on top of it.
This way, your logo will be persisted as relative path and validation should work. However, the challenge now is to determine the right moment to set that static $basePath. In reality, this one does not even need to be a class static member, but could be something global:
return MyGlobalPath::IMAGE_PATH . '/' . $this->logo;
Does this make sense?
Hope it helps a bit...
Related
On a Symfony Commandline ContainerAwareCommand I want to emulate a file upload in order to call the following Method:
namespace AppUserBundle\Services;
use PcMagas\AppImageBundle\Filters\Crop\CropFilter;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Imagine\Image\ImageInterface;
use PcMagas\AppImageBundle\Filters\Resize\ResizeToLimitsKeepintAspectRatio;
use PcMagas\AppImageBundle\Filters\Resize\ResizeParams;
use PcMagas\AppImageBundle\Loader\ImageLoaderInterface;
use PcMagas\AppImageBundle\Saver\SaverInterface;
use Imagine\Image\Box;
class ProfileImageGenerator implements UploadedFileProcessor
{
const CROP_PARAMS='crop';
/**
* #var CropFilter
*/
private $crop=null;
/**
* #var ThumbnailFilterLoader
*/
private $thumbnail=null;
/**
* #var ResizeParams
*/
private $resizeParams=null;
/**
* #var ImageLoaderInterface
*/
private $imageLoader=null;
/**
* #var SaverInterface
*/
private $imageSaver=null;
public function __construct(CropFilter $crop,
ResizeToLimitsKeepintAspectRatio $thumbnail,
ImageLoaderInterface $imageLoader,
SaverInterface $imageSaver,
$thumbnailWidth,
$thumbnailHeight
){
$this->crop=$crop;
$this->thumbnail=$thumbnail;
$this->imageLoader=$imageLoader;
$this->imageSaver=$imageSaver;
if($thumbnailWidth>0 && $this->thumbnailHeight>0){
$this->resizeParams= new Box($thumbnailWidth,$thumbnailHeight);
}
}
/**
* {#inheritDoc}
* #see \AppUserBundle\Services\UploadedFileProcessor::process()
*/
public function process(UploadedFile $f, array $params)
{
$image=$this->openUploadedFileAsImageInterface($f);
//I implement in such a manner to provide extra prossessings over thumbnail image
if(isset($params[self::CROP_PARAMS])){
$image=$this->crop->apply($image, $params[self::CROP_PARAMS]);
}
if($this->resizeParams){
$image=$this->thumbnail->apply($image,$this->resizeParams);
}
return $this->generateFileFromImageInterface($image);
}
/**
* #param UploadedFile $f
* #return ImageInterface
*/
private function openUploadedFileAsImageInterface(UploadedFile $f)
{
return $this->imageLoader($f->getContents());
}
/**
* #param ImageInterface $image
* #return Symfony\Component\HttpFoundation\File
* #throws RuntimeException
*/
private function generateFileFromImageInterface(ImageInterface $image)
{
$tmpName=tempnam(sys_get_temp_dir()).'.png';
return $this->imageSaver->save($image);
}
}
Now I want to see how the method process will behave so I created the following ContainerAwareCommand In order to emulate a file upload:
namespace AppUserBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use AppUserBundle\Services\ProfileImageGenerator;
use PcMagas\AppImageBundle\Filters\Crop\CropParams;
class CreateProfileImageCommand extends ContainerAwareCommand
{
protected function configure()
{
$this->setName('appsuserbundle:create:profile:image')
->setDecrtiption("Process a file image like image profile.")
->setHelp("This command allows you to generate a file like a process image.")
->addArgument('file',InputArgument::REQUIRED,'The image file to process.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$file=$input->getArgument('file');
/**
* #var AppUserBundle\Services\ProfileImageGenerator $container
*/
$imageGenerator=$this->getContainer()->getDefinition('app_user.thumbnail_generator');
$cropParams=new CropParams(5,5,10,10);
$file=null;//How To emulate an uploadedfile with realData?
$processedFile=$imageGenerator->process($file,[ProfileImageGenerator::CROP_PARAMS=>$cropParams])
}
}
But I am stuck on how to create an Uploadedfile from a filesystem image in order to see whether the image works do you have ansy somt of idea how to do that?
Have you looked at the Symfony Api Documentation ?
__construct(string $path, string $originalName, string|null $mimeType = null, int|null $size = null, int|null $error = null, bool $test = false)
=>
new UploadedFile($path, $originalName, $mimeType, $size, $error, $test);
Now you can insert the path you get from the command arguments and pass that through
I am trying to insert data into db ,but it shows some error like this
My model Entity
Request.php
is here `<?php
namespace EvolisClientRequest\Model\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Request
{
/**
* #var \Ramsey\Uuid\Uuid
* #ORM\Id
* #ORM\Column(type="uuid")
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Salesperson", inversedBy="request")
* #ORM\JoinTable(name="request_salesperson")
* #var Salesperson
*/
private $salesperson;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="request")
* #var Client
*/
private $client;
/**
* #ORM\ManyToMany(targetEntity="Status", inversedBy="request")
* #ORM\JoinTable(name="request_status")
* #var Status
*/
private $status;
/**
* #ORM\Column(type="integer")
* #var Qualification
*/
private $qualification;
/**
* #return \Ramsey\Uuid\Uuid
*/
public function getId()
{
return $this->id;
}
/**
* #return Salesperson
*/
public function getSalesperson()
{
return $this->salesperson;
}
/**
* #param Salesperson $salesperson
*/
public function setSalesperson($salesperson)
{
$this->salesperson = $salesperson;
}
/**
* #return Client
*/
public function getClient()
{
return $this->client;
}
/**
* #param Client $client
*/
public function setClient($client)
{
$this->client = $client;
}
/**
* #return Status
*/
public function getStatus()
{
return $this->status;
}
/**
* #param Status $status
*/
public function setStatus($status)
{
$this->status = $status;
}
/**
* #return Qualification
*/
public function getQualification()
{
return $this->qualification;
}
/**
* #param Qualification $qualification
*/
public function setQualification($qualification)
{
$this->qualification = $qualification;
}
public function __construct($salesperson, $client, $status, $qualification) {
$this->salesperson = $salesperson;
$this->client = $client;
$this->status = $status;
$this->qualification = $qualification;
}
}`
Also my
DAO "RequestBaseDao.php" is here,which is automatically generated.
<?php
/*
* This file has been automatically generated by Mouf/ORM.
* DO NOT edit this file, as it might be overwritten.
* If you need to perform changes, edit the RequestDao class instead!
*/
namespace EvolisClientRequest\Model\DAOs;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Mouf\Doctrine\ORM\Event\SaveListenerInterface;
use EvolisClientRequest\Model\Entities\Request;
/**
* The RequestBaseDao class will maintain the persistence of Request class into the request table.
*
* #method Request findByQualification($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneByQualification($fieldValue, $orderBy = null)
* #method Request findBySurfaceMin($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneBySurfaceMin($fieldValue, $orderBy = null)
* #method Request findBySurfaceMax($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneBySurfaceMax($fieldValue, $orderBy = null)
* #method Request findByPriceMin($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneByPriceMin($fieldValue, $orderBy = null)
* #method Request findByPriceMax($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneByPriceMax($fieldValue, $orderBy = null)
* #method Request findByRequestDate($fieldValue, $orderBy = null, $limit = null, $offset = null)
* #method Request findOneByRequestDate($fieldValue, $orderBy = null)
*/
class RequestBaseDao extends EntityRepository
{
/**
* #var SaveListenerInterface[]
*/
private $saveListenerCollection;
/**
* #param EntityManagerInterface $entityManager
* #param SaveListenerInterface[] $saveListenerCollection
*/
public function __construct(EntityManagerInterface $entityManager, array $saveListenerCollection = [])
{
parent::__construct($entityManager, $entityManager->getClassMetadata('EvolisClientRequest\Model\Entities\Request'));
$this->saveListenerCollection = $saveListenerCollection;
}
/**
* Get a new persistent entity
* #param ...$params
* #return Request
*/
public function create(...$params) : Request
{
$entity = new Request(...$params);
$this->getEntityManager()->persist($entity);
return $entity;
}
/**
* Peforms a flush on the entity.
*
* #param Request
* #throws \Exception
*/
public function save(Request $entity)
{
foreach ($this->saveListenerCollection as $saveListener) {
$saveListener->preSave($entity);
}
$this->getEntityManager()->flush($entity);
foreach ($this->saveListenerCollection as $saveListener) {
$saveListener->postSave($entity);
}
}
/**
* Peforms remove on the entity.
*
* #param Request $entity
*/
public function remove(Request $entity)
{
$this->getEntityManager()->remove($entity);
}
/**
* Finds only one entity. The criteria must contain all the elements needed to find a unique entity.
* Throw an exception if more than one entity was found.
*
* #param array $criteria
*
* #return Request
*/
public function findUniqueBy(array $criteria) : Request
{
$result = $this->findBy($criteria);
if (count($result) === 1) {
return $result[0];
} elseif (count($result) > 1) {
throw new NonUniqueResultException('More than one Request was found');
} else {
return;
}
}
/**
* Finds only one entity by Qualification.
* Throw an exception if more than one entity was found.
*
* #param mixed $fieldValue the value of the filtered field
*
* #return Request
*/
public function findUniqueByQualification($fieldValue)
{
return $this->findUniqueBy(array('qualification' => $fieldValue));
}
}
My RequestDao.php where i can write queries.
<?php
namespace EvolisClientRequest\Model\DAOs;
use EvolisClientRequest\Model\Entities\Request;
/**
* The RequestDao class will maintain the persistence of Request class into the request table.
*/
class RequestDao extends RequestBaseDao {
/*** PUT YOUR SPECIFIC QUERIES HERE !! ***/
public function setdata()
{
/*$product = new Request();
$product->setStatus('Keyboard');
$product->setClient('000000001ae10dda000000003c4667a6');
$product->setSalesperson('Ergonomic and stylish!');
$product->setQualification('1111');
//var_dump($r);die();
$em = $this->getEntityManager();
$em->persist($product);
$em->flush();*/
$product= $this->create('Keyboard','000000001ae10dda000000003c4667a6','Ergonomic and stylish!','1111');
$this->save($product);
}
}
Finally my Controller "ContactController.php"
<?php
namespace EvolisClientRequest\Controllers;
use EvolisClientRequest\Model\DAOs\ClientDao;
use EvolisClientRequest\Model\Entities\Client;
use EvolisClientRequest\Model\Entities\Clients;
use EvolisClientRequest\Model\DAOs\RequestDao;
use EvolisClientRequest\Model\Entities\Request;
use EvolisClientRequest\Model\Entities\Requests;
use EvolisClientRequest\Model\DAOs\SalespersonDao;
use EvolisClientRequest\Model\Entities\Salesperson;
use EvolisClientRequest\Model\Entities\Salespersons;
use Mouf\Mvc\Splash\Annotations\Get;
use Mouf\Mvc\Splash\Annotations\Post;
use Mouf\Mvc\Splash\Annotations\Put;
use Mouf\Mvc\Splash\Annotations\Delete;
use Mouf\Mvc\Splash\Annotations\URL;
use Mouf\Html\Template\TemplateInterface;
use Mouf\Html\HtmlElement\HtmlBlock;
use Psr\Log\LoggerInterface;
use \Twig_Environment;
use Mouf\Html\Renderer\Twig\TwigTemplate;
use Mouf\Mvc\Splash\HtmlResponse;
use Doctrine\DBAL\DriverManager;
use Zend\Diactoros\Response\JsonResponse;
use Doctrine\Common\Collections\ArrayCollection;
/**
* TODO: write controller comment
*/
class ContactController {
/**
* The logger used by this controller.
* #var LoggerInterface
*/
private $logger;
/**
* The template used by this controller.
* #var TemplateInterface
*/
private $template;
/**
* The header of the page.
* #var HtmlBlock
*/
private $header;
/**
* The main content block of the page.
* #var HtmlBlock
*/
private $content;
/**
* The Twig environment (used to render Twig templates).
* #var Twig_Environment
*/
private $twig;
/**
* Controller's constructor.
* #param LoggerInterface $logger The logger
* #param TemplateInterface $template The template used by this controller
* #param HtmlBlock $content The main content block of the page
* #param Twig_Environment $twig The Twig environment (used to render Twig templates)
*/
public function __construct(LoggerInterface $logger, TemplateInterface $template, HtmlBlock $content, HtmlBlock $header, Twig_Environment $twig, ClientDao $clientDao, RequestDao $requestDao, SalespersonDao $salespersonDao) {
$this->logger = $logger;
$this->template = $template;
$this->content = $content;
$this->twig = $twig;
$this->header = $header;
$this->clientDao = $clientDao;
$this->requestDao = $requestDao;
$this->salespersonDao = $salespersonDao;
}
/**
* #URL("new.html")
*/
public function new() {
// TODO: write content of action here
// Let's add the twig file to the template.
$this->content->addHtmlElement(new TwigTemplate($this->twig, 'views/contact/new.twig', array("message"=>"world")));
$this->header->addHtmlElement(new TwigTemplate($this->twig, 'views/root/header.twig', array("message"=>"world")));
return new HtmlResponse($this->template);
}
/**
* #URL("saveData")
* For Saving the data
*/
public function saveData()
{
/*$newClient = $this->clientDao->create('hello', 'sarathchandran#122.com','8907263949');
$this->clientDao->save($newClient);*/
//$data = array();
//$data['salespersonDao']['salesperson'] = 'example#sales.com';
//$data['request']['qualification'] = 'abcdefgh';
//$newClient = $this->requestDao->create($data);
//$newClient = $this->requestDao->setQualification('Keyboard');
// $this->requestDao->save($newClient);
$user_data=$this->requestDao->setdata();
//return new JsonResponse([ "status"=>0 ]);
}
}
I am using Mouf framework.I am stuck with this problem.Someone Please help me to solve this problem.
Thanks in advance
As advised by #rokas, you should really start reading more about Doctrine. This is not a Mouf issue, this is clearly a Doctrine ORM issue so the appropriate doc is here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/index.html
Here are a few tips:
Your controller calls the setdata method
$user_data=$this->requestDao->setdata();
The setdata method calls:
$product= $this->create('Keyboard','000000001ae10dda000000003c4667a6','Ergonomic and stylish!','1111');
Now, the create method is:
public function create(...$params) : Request
{
$entity = new Request(...$params);
$this->getEntityManager()->persist($entity);
return $entity;
}
This means that it will call the Request constructor with this parameters:
$entity = new Request('Keyboard','000000001ae10dda000000003c4667a6','Ergonomic and stylish!','1111');
Have a look at the Request constructor:
public function __construct($salesperson, $client, $status, $qualification) {
$this->salesperson = $salesperson;
$this->client = $client;
$this->status = $status;
$this->qualification = $qualification;
}
As you can see, the first parameter is $salesperson. You try to put the value "Keyboard" here. The $salesperson attribute is defined this way:
/**
* #ORM\ManyToMany(targetEntity="Salesperson", inversedBy="request")
* #ORM\JoinTable(name="request_salesperson")
* #var Salesperson
*/
private $salesperson;
/**
* #param Salesperson $salesperson
*/
public function setSalesperson($salesperson)
{
$this->salesperson = $salesperson;
}
Now, here is your problem I think.
The $salesperson property is defined as a "ManyToMany" association. So you really cannot put a string in here, it is a collection of Salesperson. By the way, you should not "set" anything either. The setter should be completely removed.
Instead, you should consider using it as per the documentation here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html
For instance:
$request->getSalesPersons()->add($someObjectRepresentingAPerson);
Also, notice that since the $salesperson represents a collection of Salesperson object, your should really name it $salesPersons (plural form)
I am not writing "what did I try" or "what is not working" since I can think of many ways to implement something like this. But I cannot believe that no one did something similar before and that is why I would like to ask the question to see what kind of Doctrine2 best practices show up.
What I want is to trigger an event on a property change. So let's say I have an entity with an $active property and I want a EntityBecameActive event to fire for each entity when the property changes from false to true.
Other libraries often have a PropertyChanged event but there is no such thing available in Doctrine2.
So I have some entity like this:
<?php
namespace Application\Entity;
class Entity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var boolean
* #ORM\Column(type="boolean", nullable=false)
*/
protected $active = false;
/**
* Get active.
*
* #return string
*/
public function getActive()
{
return $this->active;
}
/**
* Is active.
*
* #return string
*/
public function isActive()
{
return $this->active;
}
/**
* Set active.
*
* #param bool $active
* #return self
*/
public function setActive($active)
{
$this->active = $active;
return $this;
}
}
Maybe ChangeTracking Policy is what you want, maybe it is not!
The NOTIFY policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that purpose,
a class that wants to use this policy needs to implement the
NotifyPropertyChanged interface from the Doctrine\Common namespace.
Check full example in link above.
class MyEntity extends DomainObject
{
private $data;
// ... other fields as usual
public function setData($data) {
if ($data != $this->data) { // check: is it actually modified?
$this->onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
UPDATE
This is a full example but silly one so you can work on it as you wish. It just demonstrates how you do it, so don't take it too serious!
entity
namespace Football\TeamBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="country")
*/
class Country extends DomainObject
{
/**
* #var int
*
* #ORM\Id
* #ORM\Column(type="smallint")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(type="string", length=2, unique=true)
*/
protected $code;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set code
*
* #param string $code
* #return Country
*/
public function setCode($code)
{
if ($code != $this->code) {
$this->onPropertyChanged('code', $this->code, $code);
$this->code = $code;
}
return $this;
}
/**
* Get code
*
* #return string
*/
public function getCode()
{
return $this->code;
}
}
domainobject
namespace Football\TeamBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
abstract class DomainObject implements NotifyPropertyChanged
{
private $listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listeners[] = $listener;
}
protected function onPropertyChanged($propName, $oldValue, $newValue)
{
$filename = '../src/Football/TeamBundle/Entity/log.txt';
$content = file_get_contents($filename);
if ($this->listeners) {
foreach ($this->listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
file_put_contents($filename, $content . "\n" . time());
}
}
}
}
controller
namespace Football\TeamBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Football\TeamBundle\Entity\Country;
class DefaultController extends Controller
{
public function indexAction()
{
// First run this to create or just manually punt in DB
$this->createAction('AB');
// Run this to update it
$this->updateAction('AB');
return $this->render('FootballTeamBundle:Default:index.html.twig', array('name' => 'inanzzz'));
}
public function createAction($code)
{
$em = $this->getDoctrine()->getManager();
$country = new Country();
$country->setCode($code);
$em->persist($country);
$em->flush();
}
public function updateAction($code)
{
$repo = $this->getDoctrine()->getRepository('FootballTeamBundle:Country');
$country = $repo->findOneBy(array('code' => $code));
$country->setCode('BB');
$em = $this->getDoctrine()->getManager();
$em->flush();
}
}
And have this file with 777 permissions (again, this is test) to it: src/Football/TeamBundle/Entity/log.txt
When you run the code, your log file will have timestamp stored in it, just for demonstration purposes.
I am working with Symfony 2.6 and trying to setup PayumBundle (paypal express checkout) and I am getting an error
InvalidConfigurationException in BaseNode.php line 313: Invalid configuration for path "payum.security.token_storage": The storage entry must be a valid model class. It is set Acme\featuresBundle\Entity\PaymentToken
I am following the steps mentioned in there documetation
This is how my config.yml looks like
doctrine:
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
mappings:
payum:
is_bundle: false
type: xml
dir: %kernel.root_dir%/../vendor/payum/core/Payum/Core/Bridge/Doctrine/Resources/mapping
prefix: Payum\Core\Model
payum:
security:
token_storage:
Acme\featuresBundle\Entity\PaymentToken: { doctrine: orm }
storages:
Acme\featuresBundle\Entity\PaymentDetails: { doctrine: orm }
contexts:
paypal:
paypal_express_checkout_nvp:
username: 'asdasd'
password: 'adsasd'
signature: 'asdasdasd'
sandbox: true
This is my Entity PaymentToken
namespace Acme\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Token;
/**
* #ORM\Table
* #ORM\Entity
*/
class PaymentToken extends Token
{
}
And this is Entity PaymentDetails
namespace Acme\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Order as BaseOrder;
/**
* #ORM\Table
* #ORM\Entity
*/
class PaymentDetails extends BaseOrder
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer $id
*/
protected $id;
}
I have gone through alot of documentation online and other posts like this but I dont understand why I am getting this error.
The storage entry must be a valid model class. It is set Acme\featuresBundle\Entity\PaymentToken
I cant even get to the controller so something tells me it is the config.yml configuration of Payum that is not set correctly. I have gone through the documentation over and over and over and I cant seem to find what am I doing wrong.
I will really appreciate any help in getting pass this error.
I finally managed to get it done.
I needed 4 files
PaymentController
Orders (Entity)
PaymentToken (Entity)
Orders (Model)
This is my PaymentController looks like
<?php
namespace ClickTeck\featuresBundle\Controller;
use ClickTeck\featuresBundle\Entity\Orders;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Payum\Paypal\ExpressCheckout\Nvp\Api;
use Payum\Core\Registry\RegistryInterface;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Security\GenericTokenFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration as Extra;
class PaymentController extends Controller
{
public function preparePaypalExpressCheckoutPaymentAction(Request $request)
{
$paymentName = 'paypal';
$eBook = array(
'author' => 'Jules Verne',
'name' => 'The Mysterious Island',
'description' => 'The Mysterious Island is a novel by Jules Verne, published in 1874.',
'price' => 8.64,
'currency_symbol' => '$',
'currency' => 'USD',
'clientId' => '222',
'clientemail' => 'xyz#abc.com'
);
$storage = $this->get('payum')->getStorage('ClickTeck\featuresBundle\Entity\Orders');
/** #var $paymentDetails Orders */
$paymentDetails = $storage->create();
$paymentDetails->setNumber(uniqid());
$paymentDetails->setCurrencyCode($eBook['currency']);
$paymentDetails->setTotalAmount($eBook['price']);
$paymentDetails->setDescription($eBook['description']);
$paymentDetails->setClientId($eBook['clientId']);
$paymentDetails->setClientEmail($eBook['clientemail']);
$paymentDetails['PAYMENTREQUEST_0_CURRENCYCODE'] = $eBook['currency'];
$paymentDetails['PAYMENTREQUEST_0_AMT'] = $eBook['price'];
$paymentDetails['NOSHIPPING'] = Api::NOSHIPPING_NOT_DISPLAY_ADDRESS;
$paymentDetails['REQCONFIRMSHIPPING'] = Api::REQCONFIRMSHIPPING_NOT_REQUIRED;
$paymentDetails['L_PAYMENTREQUEST_0_ITEMCATEGORY0'] = Api::PAYMENTREQUEST_ITERMCATEGORY_DIGITAL;
$paymentDetails['L_PAYMENTREQUEST_0_AMT0'] = $eBook['price'];
$paymentDetails['L_PAYMENTREQUEST_0_NAME0'] = $eBook['author'].'. '.$eBook['name'];
$paymentDetails['L_PAYMENTREQUEST_0_DESC0'] = $eBook['description'];
$storage->update($paymentDetails);
$captureToken = $this->getTokenFactory()->createCaptureToken(
$paymentName,
$paymentDetails,
'payment_done'
);
$paymentDetails['INVNUM'] = $paymentDetails->getId();
$storage->update($paymentDetails);
return $this->redirect($captureToken->getTargetUrl());
}
public function doneAction(Request $request)
{
$token = $this->get('payum.security.http_request_verifier')->verify($request);
$payment = $this->get('payum')->getPayment($token->getPaymentName());
// you can invalidate the token. The url could not be requested any more.
// $this->get('payum.security.http_request_verifier')->invalidate($token);
// Once you have token you can get the model from the storage directly.
//$identity = $token->getDetails();
//$order = $payum->getStorage($identity->getClass())->find($identity);
// or Payum can fetch the model for you while executing a request (Preferred).
$payment->execute($status = new GetHumanStatus($token));
$order = $status->getFirstModel();
// you have order and payment status
// so you can do whatever you want for example you can just print status and payment details.
return new JsonResponse(array(
'status' => $status->getValue(),
'response' => array(
'order' => $order->getTotalAmount(),
'currency_code' => $order->getCurrencyCode(),
'details' => $order->getDetails(),
),
));
}
/**
* #return RegistryInterface
*/
protected function getPayum()
{
return $this->get('payum');
}
/**
* #return GenericTokenFactoryInterface
*/
protected function getTokenFactory()
{
return $this->get('payum.security.token_factory');
}
}
This is my Orders Entity
<?php
namespace ClickTeck\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use ClickTeck\featuresBundle\Model\Orders as BasePaymentDetails;
/**
* Orders
*/
class Orders extends BasePaymentDetails
{
/**
* #var integer
*/
protected $id;
private $number;
private $description;
private $client_email;
private $client_id;
private $total_amount;
private $currency_code;
protected $details;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set number
*
* #param integer $number
* #return Orders
*/
public function setNumber($number)
{
$this->number = $number;
return $this;
}
/**
* Get number
*
* #return integer
*/
public function getNumber()
{
return $this->number;
}
/**
* Set description
*
* #param string $description
* #return Orders
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set client_email
*
* #param string $clientEmail
* #return Orders
*/
public function setClientEmail($clientEmail)
{
$this->client_email = $clientEmail;
return $this;
}
/**
* Get client_email
*
* #return string
*/
public function getClientEmail()
{
return $this->client_email;
}
/**
* Set client_id
*
* #param string $clientId
* #return Orders
*/
public function setClientId($clientId)
{
$this->client_id = $clientId;
return $this;
}
/**
* Get client_id
*
* #return string
*/
public function getClientId()
{
return $this->client_id;
}
/**
* Set total_amount
*
* #param float $totalAmount
* #return Orders
*/
public function setTotalAmount($totalAmount)
{
$this->total_amount = $totalAmount;
return $this;
}
/**
* Get total_amount
*
* #return float
*/
public function getTotalAmount()
{
return $this->total_amount;
}
/**
* Set currency_code
*
* #param string $currencyCode
* #return Orders
*/
public function setCurrencyCode($currencyCode)
{
$this->currency_code = $currencyCode;
return $this;
}
/**
* Get currency_code
*
* #return string
*/
public function getCurrencyCode()
{
return $this->currency_code;
}
/**
* Set details
*
* #param string $details
* #return Orders
*/
public function setDetails($details)
{
$this->details = $details;
return $this;
}
/**
* Get details
*
* #return string
*/
public function getDetails()
{
return $this->details;
}
}
This is my PaymentToken Entity
<?php
namespace ClickTeck\featuresBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Payum\Core\Model\Token;
/**
* PaymentToken
*/
class PaymentToken extends Token
{
}
This is my Orders model
<?php
namespace ClickTeck\featuresBundle\Model;
use Payum\Core\Model\ArrayObject;
class Orders extends ArrayObject
{
protected $id;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
}
Now when I call the Action
preparePaypalExpressCheckoutPaymentAction via route
I get redirected to make the payment
I can see the response in doneAction
Very neat library. Took me a while to figure it out and I am glad it works now. I am sure i have alot more to learn about Payum and I hope someone can confirm if this is the right way :)
I have working entity References.php including Image, but I don't know how to in Symfony2 delete old image saved in this reference (if exists) and create new. Because now, it didn't delete current image, so only created a new and set new image_path into this entity. Here is my try to delete it on preUpload method but it set current file to NULL and then nothing (so I have error - You have to choose a file)
<?php
namespace Acme\ReferenceBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
* #ORM\Table(name="`references`")
* #ORM\HasLifecycleCallbacks
*/
class Reference
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=200)
* #Assert\NotBlank(
* message = "Name cannot be blank"
* )
* #Assert\Length(
* min = "3",
* minMessage = "Name is too short"
* )
*/
private $name;
/**
* #ORM\Column(type="string", length=200)
* #Assert\NotBlank(
* message = "Description cannot be blank"
* )
* #Assert\Length(
* min = "3",
* minMessage = "Description is too short"
* )
*/
private $description;
/**
* #ORM\Column(type="string", length=200)
* #Assert\Url(
* message = "URL is not valid"
* )
*/
private $url;
/**
* #ORM\ManyToMany(targetEntity="Material", inversedBy="references")
* #Assert\Count(min = 1, minMessage = "Choose any material")
*/
private $materials;
/**
* #ORM\Column(type="text", length=255, nullable=false)
* #Assert\NotNull(
* message = "You have to choose a file"
* )
*/
private $image_path;
/**
* #Assert\File(
* maxSize = "5M",
* mimeTypes = {"image/jpeg", "image/gif", "image/png", "image/tiff"},
* maxSizeMessage = "Max size of file is 5MB.",
* mimeTypesMessage = "There are only allowed jpeg, gif, png and tiff images"
* )
*/
private $file;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Reference
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
* #return Reference
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set url
*
* #param string $url
* #return Reference
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get url
*
* #return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set materials
*
* #param string $materials
* #return Reference
*/
public function setMaterials($materials)
{
$this->materials = $materials;
return $this;
}
/**
* Get materials
*
* #return string
*/
public function getMaterials()
{
return $this->materials;
}
/**
* Set image_path
*
* #param string $imagePath
* #return Reference
*/
public function setImagePath($imagePath)
{
$this->image_path = $imagePath;
return $this;
}
/**
* Get image_path
*
* #return string
*/
public function getImagePath()
{
return $this->image_path;
}
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* Get file.
*
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Called before saving the entity
*
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
$oldImage = $this->image_path;
$oldImagePath = $this->getUploadRootDir().'/'.$oldImage;
if (null !== $this->file) {
if($oldImage && file_exists($oldImagePath)) unlink($oldImagePath); // not working correctly
$filename = sha1(uniqid(mt_rand(), true));
$this->image_path = $filename.'.'.$this->file->guessExtension();
}
}
/**
* Called before entity removal
*
* #ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
/**
* Called after entity persistence
*
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->file) {
return;
}
// use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and then the
// target filename to move to
$this->file->move(
$this->getUploadRootDir(),
$this->image_path
);
// set the path property to the filename where you've saved the file
$this->image_path = $this->file->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->file = null;
}
protected function getAbsolutePath()
{
return null === $this->image_path
? null
: $this->getUploadRootDir().'/'.$this->image_path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded
// documents should be saved
return __DIR__.'/../../../../'.$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/references';
}
public function getWebPath()
{
return $this->getUploadDir().'/'.$this->image_path;
}
/**
* Constructor
*/
public function __construct()
{
$this->materials = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add materials
*
* #param \Acme\ReferenceBundle\Entity\Material $materials
* #return Reference
*/
public function addMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
{
$this->materials[] = $materials;
return $this;
}
/**
* Remove materials
*
* #param \Acme\ReferenceBundle\Entity\Material $materials
*/
public function removeMaterial(\Acme\ReferenceBundle\Entity\Material $materials)
{
$this->materials->removeElement($materials);
}
}
Any idea?
So I found solution. For first, I had to create an Assert callback for File Uploading, because I was using NotNull() Assert for Reference entity. So if I selected any file and sent form, I was always getting error You have to choose a file. So my first edit was here:
use Symfony\Component\Validator\ExecutionContextInterface; // <-- here
/**
* #ORM\Entity(repositoryClass="Acme\ReferenceBundle\Entity\ReferenceRepository")
* #ORM\Table(name="`references`")
* #ORM\HasLifecycleCallbacks
* #Assert\Callback(methods={"isFileUploadedOrExists"}) <--- and here
*/
class Reference
{
// code
}
and then in my code add a new method:
public function isFileUploadedOrExists(ExecutionContextInterface $context)
{
if(null === $this->image_path && null === $this->file)
$context->addViolationAt('file', 'You have to choose a file', array(), null);
}
Also I deleted NotNull assertion in my $image_path property.
Then it was working successfuly - if I selected a file and submitted the form, reference was created with image. But it wasn't finished yet. There was my problem which I asked in this question - delete old image and create a new image with new path, of course.
After many experiments, i found the working and good looking solution. In my controller, I added one variable before form validation and after it is used to delete old image:
$oldImagePath = $reference->getImagePath(); // get path of old image
if($form->isValid())
{
if ($form->get('file')->getData() !== null) { // if any file was updated
$file = $form->get('file')->getData();
$reference->removeFile($oldImagePath); // remove old file, see this at the bottom
$reference->setImagePath($file->getClientOriginalName()); // set Image Path because preUpload and upload method will not be called if any doctrine entity will not be changed. It tooks me long time to learn it too.
}
$em->persist($reference);
try {
$em->flush();
} catch (\PDOException $e) {
//sth
}
And my removeFile() method:
public function removeFile($file)
{
$file_path = $this->getUploadRootDir().'/'.$file;
if(file_exists($file_path)) unlink($file_path);
}
And at the end, I deleted $this->image_path = $this->file->getClientOriginalName(); line in upload() method because it causes a problem with preview image in the form, if you use any. It sets an original file name as path, but if you reload page, you will see the real path of image. Removing this line will fix the problem.
Thanks everyone to posting answers, who helps me to find the solution.
If the image_path is already set there is an "old" image you want to replace.
Inside your upload() method instead of ...
// set the path property to the filename where you've saved the file
$this->image_path = $this->file->getClientOriginalName();
... check for existance of a previous file and remove it before:
if ($this->image_path) {
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
$this->image_path = $this->file->getClientOriginalName();
the #Assert\NotNull on the image_path property is tested before your PrePersist/PreUpdate method, so the form validation is not happy because image_path is only provided in the entity internal, the request does not provide the form with "image_path" property, I think you should remove this Assert which is not really useful I think since it is not linked to a form.
OR
your old image_path is the fresh one, and not the old one because it is processed after form binding.
You should use event listeners, which are way better then annotation events in entities, so that you will be able in preUpdate event to retrieve the right values.
You could the use methods like these:
hasChangedField($fieldName) to check if the given field name of the current entity changed.
getOldValue($fieldName) and getNewValue($fieldName) to access the values of a field.
setNewValue($fieldName, $value) to change the value of a field to be updated.