I have a strange problem with a relation between two entities:
One Joboffer can have many jobofferLocations, while Many jobOfferlocations only have one joboffer:
class Joboffer
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(name="id_joboffer", type="integer", length=255, nullable=false)
* #Groups({"api_read", "api_write"})
*/
protected $id;
/**
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Joboffer\JobofferLocation", mappedBy="joboffer", orphanRemoval=true, cascade={"persist"})
* #Groups({"api_read", "api_write"})
* #var ArrayCollection
*/
protected $jobofferLocations;
....
/**
* #param JobofferLocation $jobofferLocation
*/
public function addJobofferLocation(JobofferLocation $jobofferLocation)
{
if ($this->jobofferLocations->contains($jobofferLocation)) {
return;
}
$this->jobofferLocations->add($jobofferLocation);
$jobofferLocation->setJoboffer($this);
}
The jobofferlocationclass:
class JobofferLocation
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(name="id_joboffer_location", type="integer", length=255, nullable=false)
* #Groups({"api_read"})
*/
protected $id;
/**
* #return mixed
*/
public function getJoboffer()
{
return $this->joboffer;
}
/**
* #param mixed $joboffer
*/
public function setJoboffer($joboffer)
{
$this->joboffer = $joboffer;
}
On Updates I have this problem:
When I use "orphanRemoval=true,", it removes all jobofferlocation entities, when I don't use it, but "cascade=remove", it doesn't remove the ones that aren't in the relations any more.
So, is there a way to update all relations? (Remove the ones that aren't needed any more, adding new ones and updating existing ones.)
I found an answer:
first of all, the methods addJObofferLocation and removeJobofferLocation are needed and orphanRemoval must be set to true.
The trick seems to be in adding the right (not double) locations.
class Joboffer
{
...
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Joboffer\JobofferLocation", mappedBy="joboffer", orphanRemoval=true,cascade={"persist"})
* #Groups({"api_read", "api_write"})
* #var ArrayCollection
*/
protected $jobofferLocations;
/**
* #param JobofferLocation $jobofferLocation
*/
public function addJobofferLocation(JobofferLocation $jobofferLocation)
{
if ($this->jobofferLocations->contains($jobofferLocation)) {
return;
}
/** #var JobofferLocation $location */
foreach ($this->jobofferLocations as $location){
//check if this location exists
// it seems we need this, because of the API plattform bundle
if ($location->getIdLocation() == $jobofferLocation->getIdLocation()){
// if it exists, just copy the new jobofferlocation settings
return;
}
}
$jobofferLocation->setJoboffer($this);
$this->jobofferLocations->add($jobofferLocation);
}
public function removeJobOfferLocation(JobofferLocation $jobofferLocation)
{
if (!$this->jobofferLocations->contains($jobofferLocation)) {
return;
}
$this->jobofferLocations->removeElement($jobofferLocation);
$jobofferLocation->removeJobOffer();
}
Related
I cannot figure out how to reach this goal:
I want to have the object representation in a GET request with the related entity as json object too, while i want to send just the iri in a POST request of the parent object
/**
* #ORM\Entity(repositoryClass=CustomerRepository::class)
* #ApiResource
*/
class Customer
{
/**
* #var int|null
*
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #ApiProperty(
* identifier=false,
* description="ID univoco del cliente generato da un'operazione di POST"
* )
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="codice_cliente", type="string", length=20, unique=true)
* #ApiProperty(
* identifier=true,
* description="Codice identificativo del cliente.<br>Si tratta di un codice univoco che identifica il cliente all'interno dell'azienda di riferimento"
* )
*/
private $codiceCliente;
/**
* #var string|null
*
* #ORM\Column(name="ragione_sociale", type="string", length=50, nullable=true)
* #ApiProperty(description="Denominazione ufficiale del cliente")
*/
private $ragioneSociale;
/**
* #var string|null
*
* #ORM\Column(name="nome", type="string", length=50, nullable=true)
* #ApiProperty(description="Nome anagrafico del cliente")
*/
private $nome;
/**
* #var string|null
*
* #ORM\Column(name="cognome", type="string", length=50, nullable=true)
* #ApiProperty(description="Cognome anagrafico del cliente")
*/
private $cognome;
/**
* #var string|null
*
* #ORM\Column(name="codice_fiscale", type="string", length=16, nullable=true)
* #ApiProperty(description="Codice fiscale del cliente.<br>Può corrispondere a `partitaIva` in caso di azienda.")
*/
private $codiceFiscale;
/**
* #var string|null
*
* #ORM\Column(name="partita_iva", type="string", length=11, nullable=true)
* #ApiProperty(description="Partita Iva del cliente in caso di azienda.<br>È un valore numerico di 11 cifre.")
*/
private $partitaIva;
/**
* #var CustomerCategory
*
* #ORM\ManyToOne(targetEntity=CustomerCategory::class)
* #ORM\JoinColumn(name="categoria_id", referencedColumnName="codice", nullable=false)
*/
private $categoria;
/**
* #return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* #return string
*/
public function getCodiceCliente(): ?string
{
return $this->codiceCliente;
}
/**
* #param string $codiceCliente
*
* #return Customer
*/
public function setCodiceCliente(string $codiceCliente): Customer
{
$this->codiceCliente = $codiceCliente;
return $this;
}
/**
* #return string|null
*/
public function getRagioneSociale(): ?string
{
return $this->ragioneSociale;
}
/**
* #param string|null $ragioneSociale
*
* #return Customer
*/
public function setRagioneSociale(?string $ragioneSociale): Customer
{
$this->ragioneSociale = $ragioneSociale;
return $this;
}
/**
* #return string|null
*/
public function getNome(): ?string
{
return $this->nome;
}
/**
* #param string|null $nome
*
* #return Customer
*/
public function setNome(?string $nome): Customer
{
$this->nome = $nome;
return $this;
}
/**
* #return string|null
*/
public function getCognome(): ?string
{
return $this->cognome;
}
/**
* #param string|null $cognome
*
* #return Customer
*/
public function setCognome(?string $cognome): Customer
{
$this->cognome = $cognome;
return $this;
}
/**
* #return string|null
*/
public function getCodiceFiscale(): ?string
{
return $this->codiceFiscale;
}
/**
* #param string|null $codiceFiscale
*
* #return Customer
*/
public function setCodiceFiscale(?string $codiceFiscale): Customer
{
$this->codiceFiscale = $codiceFiscale;
return $this;
}
/**
* #return string|null
*/
public function getPartitaIva(): ?string
{
return $this->partitaIva;
}
/**
* #param string|null $partitaIva
*
* #return Customer
*/
public function setPartitaIva(?string $partitaIva): Customer
{
$this->partitaIva = $partitaIva;
return $this;
}
public function getCategoria(): ?CustomerCategory
{
return $this->categoria;
}
public function setCategoria(?CustomerCategory $categoria): self
{
$this->categoria = $categoria;
return $this;
}
}
/**
* #ORM\Entity(repositoryClass=CustomerCategoryRepository::class)
* #ApiResource()
*/
class CustomerCategory
{
/**
* #var string
*
* #ORM\Id
* #ORM\Column(type="string", length=10)
* #ApiProperty(identifier=true,
* description="Codice identificativo della categoria")
*/
private $codice;
/**
* #var string
*
* #ORM\Column(type="string", length=100, nullable=true)
* #ApiProperty(description="Descrizione del codice di identificazione")
*/
private $descrizione;
public function getCodice(): ?string
{
return $this->codice;
}
public function setCodice(string $codice): self
{
$this->codice = $codice;
return $this;
}
public function getDescrizione(): ?string
{
return $this->descrizione;
}
public function setDescrizione(?string $descrizione): self
{
$this->descrizione = $descrizione;
return $this;
}
}
So when i GET the Customer i want the $categoria property to be
{
codice: "10"
descrizione: "Test"
}
While when i POST/PUT/PATCH a Customer i want to refer to the CustomerCategory as the IRI of the ID as i don't have cascade persists in this case
{
"codiceCliente": "string",
"ragioneSociale": "string",
"nome": "string",
"cognome": "string",
"codiceFiscale": "string",
"partitaIva": "string",
"categoria": "/api/customer_categories/10"
}
I want also to have the correct representation on the OPEN API docs
You can get such results using a Serialization Group, except for the #id and #type being added to both Customer and to the $categoria property (API Platform automatically adds them).
First add the following to both your Entities:
use Symfony\Component\Serializer\Annotation\Groups;
To configure a Serialization Group only for the get operations of Customer:
/**
* #ORM\Entity(repositoryClass=CustomerRepository::class)
* #ApiResource(
* itemOperations={
* "get"={
* "normalization_context"={"groups"={"customer:get"}}
* },
* "patch",
* "put",
* "delete"
* },
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"customer:get"}}
* },
* "post"
* }
* )
*/
class Customer
//...
And then add #Groups({"customer:get"}) to the doc blocks of all props of Customer except $id, including $categoria. I only give one example here:
/**
* #var string|null
*
* #ORM\Column(name="ragione_sociale", type="string", length=50, nullable=true)
* #ApiProperty(description="Denominazione ufficiale del cliente")
* #Groups({"customer:get"})
*/
private $ragioneSociale;
Also add #Groups({"customer:get"}) to the doc blocks of $codice and $descrizione of CustomerCategory.
This should do the trick and leave the post, put and patch operations unchanged (expecting and returning just the iri of $categoria). It should automatically give a correct representation on the OPEN API docs.
Tip: The readme of branch chapter4-api of my tutorial also contains instructions for adding Serialization Groups two Entities related like yours, and the resulting code is in branch chapter5-api.
/*When I try to view the output of my project on the browser, I am redirected to the following error
"ErrorException (E_WARNING)rtrim () expects the parameter 1 to be
string, given the object ".
Can someone help me?
*/
i find the problem in following function public function setBasePath($basePath ){
below the code Application.php:
<?php
namespace Illuminate\Foundation;
use Closure;
use RuntimeException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Container\Container;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Log\LogServiceProvider;
use Illuminate\Support\ServiceProvider;
use Illuminate\Events\EventServiceProvider;
use Illuminate\Routing\RoutingServiceProvider;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Contracts\Http\Kernel as HttpKernelContract;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
/**
* The Laravel framework version.
*
* #var string
*/
const VERSION = '5.6.39';
/**
* The base path for the Laravel installation.
*
* #var string
*/
protected $basePath;
/**
* Indicates if the application has been bootstrapped before.
*
* #var bool
*/
protected $hasBeenBootstrapped = false;
/**
* Indicates if the application has "booted".
*
* #var bool
*/
protected $booted = false;
/**
* The array of booting callbacks.
*
* #var array
*/
protected $bootingCallbacks = [];
/**
* The array of booted callbacks.
*
* #var array
*/
protected $bootedCallbacks = [];
/**
* The array of terminating callbacks.
*
* #var array
*/
protected $terminatingCallbacks = [];
/**
* All of the registered service providers.
*
* #var array
*/
protected $serviceProviders = [];
/**
* The names of the loaded service providers.
*
* #var array
*/
protected $loadedProviders = [];
/**
* The deferred services and their providers.
*
* #var array
*/
protected $deferredServices = [];
/**
* The custom database path defined by the developer.
*
* #var string
*/
protected $databasePath;
/**
* The custom storage path defined by the developer.
*
* #var string
*/
protected $storagePath;
/**
* The custom environment path defined by the developer.
*
* #var string
*/
protected $environmentPath;
/**
* The environment file to load during bootstrapping.
*
* #var string
*/
protected $environmentFile = '.env';
/**
* The application namespace.
*
* #var string
*/
protected $namespace;
/**
* Create a new Illuminate application instance.
*
* #param string|null $basePath
* #return void
*/
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
$this->registerBaseBindings();
$this->registerBaseServiceProviders();
$this->registerCoreContainerAliases();
}
/**
* Get the version number of the application.
*
* #return string
*/
public function version()
{
return static::VERSION;
}
/**
* Register the basic bindings into the container.
*
* #return void
*/
protected function registerBaseBindings()
{
static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
$this->instance(PackageManifest::class, new PackageManifest(
new Filesystem, $this->basePath(), $this->getCachedPackagesPath()
));
}
/**
* Register all of the base service providers.
*
* #return void
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
/**
* Run the given array of bootstrap classes.
*
* #param array $bootstrappers
* #return void
*/
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->fire('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->fire('bootstrapped: '.$bootstrapper, [$this]);
}
}
/**
* Register a callback to run after loading the environment.
*
* #param \Closure $callback
* #return void
*/
public function afterLoadingEnvironment(Closure $callback)
{
return $this->afterBootstrapping(
LoadEnvironmentVariables::class, $callback
);
}
/**
* Register a callback to run before a bootstrapper.
*
* #param string $bootstrapper
* #param \Closure $callback
* #return void
*/
public function beforeBootstrapping($bootstrapper, Closure $callback)
{
$this['events']->listen('bootstrapping: '.$bootstrapper, $callback);
}
/**
* Register a callback to run after a bootstrapper.
*
* #param string $bootstrapper
* #param \Closure $callback
* #return void
*/
public function afterBootstrapping($bootstrapper, Closure $callback)
{
$this['events']->listen('bootstrapped: '.$bootstrapper, $callback);
}
/**
* Determine if the application has been bootstrapped before.
*
* #return bool
*/
public function hasBeenBootstrapped()
{
return $this->hasBeenBootstrapped;
}
/**
* Set the base path for the application.
*
* #param string $basePath
* #return $this
*/
public function setBasePath($basePath )
{
$this->basePath = rtrim($basePath, '\/');
$this->bindPathsInContainer();
return $this;
}
/**
* Bind all of the application paths in the container.
*
* #return void
*/
protected function bindPathsInContainer()
{
$this->instance('path', $this->path());
$this->instance('path.base', $this->basePath());
$this->instance('path.lang', $this->langPath());
$this->instance('path.config', $this->configPath());
$this->instance('path.public', $this->publicPath());
$this->instance('path.storage', $this->storagePath());
$this->instance('path.database', $this->databasePath());
$this->instance('path.resources', $this->resourcePath());
$this->instance('path.bootstrap', $this->bootstrapPath());
}
/**
* Get the path to the application "app" directory.
*
* #param string $path Optionally, a path to append to the app path
* #return string
*/
public function path($path = '')
{
return $this->basePath.DIRECTORY_SEPARATOR.'app'.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
* Get the base path of the Laravel installation.
*
* #param string $path Optionally, a path to append to the base path
* #return string
*/
public function basePath($path = '')
{
return $this->basePath.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
* Get the path to the bootstrap directory.
*
* #param string $path Optionally, a path to append to the bootstrap path
* #return string
*/
public function bootstrapPath($path = '')
{
return $this->basePath.DIRECTORY_SEPARATOR.'bootstrap'.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
* Get the path to the application configuration files.
*
* #param string $path Optionally, a path to append to the config path
* #return string
*/
public function configPath($path = '')
{
return $this->basePath.DIRECTORY_SEPARATOR.'config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
* Get the path to the database directory.
*
* #param string $path Optionally, a path to append to the database path
* #return string
*/
public function databasePath($path = '')
{
return ($this->databasePath ?: $thi`enter code here`s->basePath.DIRECTORY_SEPARATOR.'database').($path ? DIRECTORY_SEPARATOR.$path : $path);
}
Can you check you .env for APP_BASE_PATH?
The Application is initialized in bootstrap/app.php file
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
As you can see it looks APP_BASE_PATH environment variable and if this is not set it uses the path to your root application folder.
dirname(__DIR__) returns the path to the parent folder.
Based on the details you posted it looks like the problem can be with the value you set for APP_BASE_PATH
This response assumes you are using the latest version of Laravel, 5.8
I try to create a tree structure for a catalog of products.
A catalog can have multiple levels and levels can contain multiple products.
I manage to save my structure in database but when I want to edit it, I have this error :
Error : Missing value for primary key catalogCode on AppBundle\Entity\CatalogLevel
at OutOfBoundsException ::missingPrimaryKeyValue ('AppBundle\Entity\CatalogLevel', 'catalogCode')
in vendor\doctrine\common\lib\Doctrine\Common\Proxy\AbstractProxyFactory.php at line 125
when I do this in my CatalogController :
$form = $this->createForm(CatalogTreeType::class, $catalog);
But, just before that line, I verify if I get my levels correctly and it's looking like that's the case :
// Create an ArrayCollection of the current levels
$originalLevels = new ArrayCollection();
foreach ($catalog->getLevels() as $level) {
var_dump($level->getCatalogCode());
$originalLevels->add($level);
}
// returns
AppBundle\Controller\CatalogController.php:337:string 'TT-FTEST' (length=8)
AppBundle\Controller\CatalogController.php:337:string 'TT-FTEST' (length=8)
CatalogLevel entity has a composite key : levelId + catalogCode.
Considering the primary key catalogCode isn't empty, I don't understand this error...
Catalog Entity
/**
* #ORM\Table(name="catalogue")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogRepository")
* #UniqueEntity(fields="code", message="Catalog code already exists")
*/
class Catalog
{
/**
* #ORM\Column(name="Catalogue_Code", type="string", length=15)
* #ORM\Id
* #Assert\NotBlank()
* #Assert\Length(max=15, maxMessage="The code is too long ({{ limit }} characters max)")
*/
private $code;
/**
* #ORM\OneToMany(targetEntity="CatalogLevel", mappedBy="catalog", cascade={"persist", "remove"})
* #Assert\Valid
*/
private $levels;
/**
* Constructor
*/
public function __construct()
{
$this->levels = new ArrayCollection();
}
/**
* Get levels
*
* #return ArrayCollection
*/
public function getLevels()
{
return $this->levels;
}
/**
* Add level
*
* #param \AppBundle\Entity\CatalogLevel $level
*
* #return Catalog
*/
public function addLevel(\AppBundle\Entity\CatalogLevel $level)
{
$level->setCatalogCode($this->getCode());
$level->setCatalog($this);
if (!$this->getLevels()->contains($level)) {
$this->levels->add($level);
}
return $this;
}
/**
* Remove level
*
* #param \AppBundle\Entity\CatalogLevel $level
*/
public function removeLevel(\AppBundle\Entity\CatalogLevel $level)
{
$this->levels->removeElement($level);
}
}
CatalogLevel Entity
/**
* #ORM\Table(name="catalogue_niveau")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogLevelRepository")
*/
class CatalogLevel
{
/**
* #ORM\Column(name="Niveau_ID", type="string", length=15)
* #ORM\Id
*/
private $id;
/**
* #ORM\Column(name="Catalogue_Code", type="string", length=15)
* #ORM\Id
*/
private $catalogCode;
/**
* #ORM\ManyToOne(targetEntity="Catalog", inversedBy="levels")
* #ORM\JoinColumn(name="Catalogue_Code", referencedColumnName="Catalogue_Code")
*/
private $catalog;
/**
* Set id
*
* #param string $id
*
* #return CatalogLevel
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get id
*
* #return string
*/
public function getId()
{
return $this->id;
}
/**
* Set catalogCode
*
* #param string $catalogCode
*
* #return CatalogLevel
*/
public function setCatalogCode($catalogCode)
{
$this->catalogCode = $catalogCode;
return $this;
}
/**
* Get catalogCode
*
* #return string
*/
public function getCatalogCode()
{
return $this->catalogCode;
}
}
I would like to remind you that this error occured on the editAction (it works very well on the addAction) when I display the pre-filled form.
Thanks for your help !
I think that is because you haven't autoincrement id in entity CatalogLevel. Try add to id this code:
#ORM\GeneratedValue(strategy="AUTO")
You have some problems in the way you've created you Entities. You should use auto generate strategy. Also the "#ORM\Id" annotation is the unique identifier.
Also, your "JoinColumn" is incorrect. You need to refer back to the "Catalog" Entity, and it's id (identifier). There is no need for 2 "#ORM\Id" entries in the class CatalogLevel.
So make these changes:
/**
* #ORM\Table(name="catalog")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogRepository")
*/
class Catalog
{
/**
* #ORM\Column(name="cat_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $cat_id;
/**
* #ORM\OneToMany(targetEntity="CatalogLevel", mappedBy="catalog", cascade={"persist", "remove"})
* #Assert\Valid
*/
private $levels;
...
/**
* #ORM\Table(name="catalog_level")
* #ORM\Entity(repositoryClass="AppBundle\Entity\CatalogLevelRepository")
*/
class CatalogLevel
{
/**
* #ORM\Column(name="cat_level_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $cat_level_id;
/**
* #ORM\ManyToOne(targetEntity="Catalog", inversedBy="levels")
* #ORM\JoinColumn(name="local_cat_id", referencedColumnName="cat_id")
*/
private $catalog;
...
I have some difficulties about applying validation for only one associated entity.
So I have two entities, News and NewsTranslation. A news could be translated in multiple languages. But I would like to apply validation only if locale is en.
// AppBundle/Entity/News.php
class News
{
use ORMBehaviors\Translatable\Translatable;
use ORMBehaviors\Timestampable\Timestampable;
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var int
*
* #ORM\Column(name="status", type="smallint")
* #Assert\NotBlank
*/
private $status;
...
}
// AppBundle/Entity/NewsTranslation.php
class NewsTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\NotBlank
* #Assert\Length(max = 255)
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="text", type="string", nullable=true)
* #Assert\NotBlank
*/
private $text;
}
# AppBundle/Resources/config/validation.yml
AppBundle\Entity\News:
properties:
translations:
- Valid: ~
I tried to use a Closure for the validation_groups form option. But it looks like Symfony do validation on News entity and Valid constraint apply the same groups on NewsTranslation.
I know I could use Callback constraint but that's mean to redo NotBlank, Length and other exiting constraints by myself. And I would like to avoid it if possible.
EDIT:
I'm using Symfony 2.8.*
I try using an en validation group. But looks like the validation is launch on News entity with validation_groups. And with Valid constraint the en validation group is given to validate NewsTranlation. So even it's the en or fr translation the group change nothing in this case.
I also try using the validation medatada through an #Assert\Callback or by using loadValidatorMetadata method into NewsTranslation entity. And the problem stay similar. I can't apply an constraint for a specific entity of collection.
I finally found a way by creating a custom validator.
Like this I could use core constraints easily.
In the translation entity, I could use my validator like this:
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, nullable=true)
* #Assert\Length(max = 255)
* #AppAssert\ValidTranslation(locales = {"fr"}, constraints = {
* #Assert\NotBlank
* })
*/
private $title;
And the validator:
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraints\Composite;
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* #author Nicolas Brousse
*/
class ValidTranslation extends Composite
{
public $locales = array();
public $constraints = array();
public function getCompositeOption()
{
return 'constraints';
}
public function getRequiredOptions()
{
return array('locales', 'constraints');
}
}
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* #author Nicolas Brousse
*/
class ValidTranslationValidator extends ConstraintValidator
{
/**
* If property constraint
* {#inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidTranslation) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\ValidTranslation');
}
if (false) { // #todo check by interface or trait
throw new UnexpectedTypeException($value, 'not a translation entity');
}
$context = $this->context;
$entity = $this->context->getObject();
if (in_array($entity->getLocale(), $constraint->locales)) {
$context = $this->context;
if ($context instanceof ExecutionContextInterface) {
$validator = $context->getValidator()->inContext($context);
$validator->validate($value, $constraint->constraints);
} else {
// 2.4 API
$context->validateValue($value, $constraint->constraints);
}
}
}
}
you form need to return 2 validations_groups, "Default" and the validation group corresponding to the "en" locale
Hi guys i have implemented vichuploadbundle to upload images but i want in the admin the user to remove the image.
<?php
namespace George\PageBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use George\UserBundle\Entity\User;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Page
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="George\PageBundle\Entity\PageRepository")
* #Vich\Uploadable
*/
class Page
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="text")
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="content", type="text")
*/
private $content;
/**
* #var boolean
*
* #ORM\Column(name="visible", type="boolean")
*/
private $visible;
/**
* #var \DateTime
*
* #ORM\Column(name="created", type="datetime")
*/
private $created;
/**
* #var \DateTime
*
* #ORM\Column(name="modefied", type="datetime")
*/
private $modefied;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=255)
*/
private $description;
/**
* #var string
*
* #ORM\Column(name="keywords", type="string", length=255)
*/
private $keywords;
/**
* #ORM\ManyToOne(targetEntity="George\UserBundle\Entity\User", inversedBy="pages")
* #ORM\JoinColumn(onDelete="CASCADE")
*/
private $owner;
//#ORM\Column(length=128, unique=true)
/**
* #Gedmo\Slug(fields={"title"})
* #ORM\Column(length=128, unique=true)
*/
private $slug;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*
* #return Page
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set content
*
* #param string $content
*
* #return Page
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* #return string
*/
public function getContent()
{
return $this->content;
}
/**
* Set visible
*
* #param boolean $visible
*
* #return Page
*/
public function setVisible($visible)
{
$this->visible = $visible;
return $this;
}
/**
* Get visible
*
* #return boolean
*/
public function getVisible()
{
return $this->visible;
}
/**
* Set created
*
* #param \DateTime $created
*
* #return Page
*/
public function setCreated($created)
{
$this->created = $created;
return $this;
}
/**
* Get created
*
* #return \DateTime
*/
public function getCreated()
{
return $this->created;
}
/**
* Set modefied
*
* #param \DateTime $modefied
*
* #return Page
*/
public function setModefied($modefied)
{
$this->modefied = $modefied;
return $this;
}
/**
* Get modefied
*
* #return \DateTime
*/
public function getModefied()
{
return $this->modefied;
}
/**
* Set description
*
* #param string $description
*
* #return Page
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set keywords
*
* #param string $keywords
*
* #return Page
*/
public function setKeywords($keywords)
{
$this->keywords = $keywords;
return $this;
}
/**
* Get keywords
*
* #return string
*/
public function getKeywords()
{
return $this->keywords;
}
/**
* #return mixed
*/
public function getOwner()
{
return $this->owner;
}
/**
* #param mixed $owner
*/
public function setOwner(User $owner)
{
$this->owner = $owner;
}
public function getSlug()
{
return $this->slug;
}
// ..... other fields
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="product_image", fileNameProperty="imageName")
*
* #var File
*/
private $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
private $updatedAt;
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*/
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
if ($image) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTime('now');
}
}
/**
* #return File
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
* #param string $imageName
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
}
/**
* #return string
*/
public function getImageName()
{
return $this->imageName;
}
}
Can you advice me what is the best solution to this. I think it will be best if i have a method remove image and in the method try to use bundles build in function for removal the image (which i don't find anywhere) and update it in the database. In the front end this can be don trough ajax or on submit... Is there a build in function for the removal of file in the vichuploadbundle?
I have found how to remove the image. The bundle provides a custom form type in order to ease the upload, deletion and download of images.
When you build the form you specifies the widget:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('image', 'vich_image', array(
'required' => false,
'allow_delete' => true, // not mandatory, default is true
'download_link' => true, // not mandatory, default is true
));
}
This add a twig template for deleting the img :)