Symfony4: Delete issue when OneToMany and OneToOne on the same entity - doctrine

I have a Trick entity who contains a OneToOne ($mainImage) and OneToMany ($images) association with the same entity.
I can't delete a Trick: it tells me there is a foreign key constraint.
Of course, because the $mainImage has a FK for Image, and Image has a foreign key for Trick !
If I manually empty the $mainImage and flush before deletion, it works, but it's to tricky, I can't bear such irrespect toward the clineliness of my code!
I think there is a thing to do around "cascade" or "orphanRemoval" attributes of Doctrine but, as you can see below, I've tried them all and I still get the error.
class Trick
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Image", mappedBy="trick", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $images;
/**
* #ORM\OneToOne(targetEntity="App\Entity\Image", cascade={"persist", "remove"}, orphanRemoval=true)
* #JoinColumn(name="main_image_id", referencedColumnName="id", onDelete="set null")
*/
private $mainImage;
}
and an Image entity:
class Image
{
/**
* #ORM\ManyToOne(targetEntity="Trick", inversedBy="images")
* #ORM\JoinColumn(nullable=false)
*/
private $trick;
}
Could someone help me please ?
You'll get all my gratitude !

You have to remove the element in your setter first
public function setImage($image){
if($this->image !== null) // first remove old one from images
$this->images->removeElement($this->image);
$this->image = $image;
}
the same holds vice versa: if you want to remove an image from the array, first check if its your current image and delete it too.

Related

The eloquent builder always attached orderby 'id' even after I have different PrimaryKey.How to get it solve?

I have my primaryKey as member_id (not id which is by default in a model) for my table Members. I have done the necessary changes in the model as follows.
class Member extends Model
{
use HasFactory;
public $timestamps = false;
/**
* The primary key for the model.
*
* #var string
*/
protected $primaryKey = 'member_id';
/**
* The "type" of the auto-incrementing ID.
*
* #var string
*/
protected $keyType = 'string';
public $incrementing = false;
but Member::query() always attach orderby id asc at the end of the query.
So I get errors for unknown column id. I have tried to reorder from my query but in vain as it is always attached at the end of my query.
How can i get the expected result?
After a long struggle with the code, I hunt down the main reason why it is not working as aspected.And thought to post the solution here so that someone like me do not waste time, in finding whats wrong.
I had used Powergrid DataTable Package here and found this
By default, PowerGrid uses the field id as your Model’s primary key.
If your model uses a custom primary key, you must configure the property
$primaryKey in your PowerGrid component.
final class MemberList extends PowerGridComponent
{
use ActionButton;
public string $primaryKey = 'members.member_id';
public string $sortField = 'members.member_id';
public function datasource(): Builder
{
return Member::query()
}
//..
//..
Now with this changes in the powergrid datatable file (where I fired the Member.query() function, it is working fine now.

Laravel Eloquent collection of mixed models

I have a database with a table called files. Within that, we have the following structure -
- id
- parent_id (nullable)
- name
- type (enum: File or Folder)
- created_at
- updated_at
I then have two models, one called File and one called Folder. Folder extends File. Is there a way that when I call File::all(), for example, I can utilize Eloquent to map the respective models based on the databases type field?
Eloquent returns collection instances, so one way would be to call map() and have that return the appropriate objects for each item, eg, if it's a file just return the file, whereas if it's a folder populate a new Folder instance and return it.
Or you could have File and Folder be models that work off the same table, with global scopes used to limit the query set by type, then call all() on both of them and merge them.
But I think the best thing to do with them is make them a single model, that behaves differently based on the type. Put any differing functionality in methods on the model so it can be treated the same regardless of type by calling those methods. I think that's the safer option in that you're making your models polymorphic - they can be treated the same regardless of type.
I've been able to work out the answer by extending Laravel Models newFromBuilder method.
Here is my class -
class File {
public static $types = ['File', 'Folder'];
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param null $connection
*
* #return Model|string
*/
public function newFromBuilder($attributes = [], $connection = null)
{
$model = $this->newInstanceFromType($attributes->type);
$model->exists = true;
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
/**
* Determine our model instance based on the type field.
*
* #param string $type
*
* #return mixed
*/
private function newInstanceFromType(string $type)
{
if (!in_array($type, static::$types)) {
throw new InvalidArgumentException('$type must be one of static::$types');
}
$model = 'App\Models\\' . $type;
return new $model;
}
}
This will then return either a File or Folder model instance depending on what the type enum is from the database.
Thanks all for the input!

Laravel - formatting all dates simultaneously

I'm currently working on a POC to showcase that it's going to be fairly painless to create an API with Laravel, the catch being that the database is already set in stone.
One problem I've run into is that they've used custom created at and updated at column names, e.g. for a car table, the created_at column would be car_time and the updated date would be cardata_time, and these are all saved as unix timestamps.
I know you can set the CREATED_AT and UPDATED_AT columns for each model. I want to go another step and return all dates in ISO 8601 format.
I've inserted a class between my models and Model called MasterModel and I want to do something like
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
to make all created at dates be in that format. The problem is that I the custom created at and updated columns mean that this never gets called.
Is there a way for me to identify the created at and updated at columns in such a way that I can use a single method to updated all created at dates at the same time?
UPDATE: I realize my original question was not clear enough - I need to identify all fields that are dates, not just created_at and updated_at, and have them formatted a certain way. They will always be unix timestamps. Not sure how I'd go about this.
Here an answer that will expand on #caddy dz answer who happen to be sitting with me.
 All the things that need to be known
Deactivation of auto management of timestamps
public $timestamps = false; // <-- deactivate the automatic handling
Change table attributes names
const CREATED_AT = 'creation_date'; // <--- change the names
const UPDATED_AT = 'last_update';
source doc:
https://laravel.com/docs/5.8/eloquent#eloquent-model-conventions
By default, Eloquent expects created_at and updated_at columns to
exist on your tables. If you do not wish to have these columns
automatically managed by Eloquent, set the $timestamps property on
your model to false:
 Creating the accessors
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
// do whatever you want here (change and mutate the value)
return ucfirst($value);
}
}
First thing to know, is that the accessors are a global concept for
eloquent and can be writing for all attributes and not just
getCreatedAtAttribute or getUpdatedAtAttribute.
Second thing to know is that whatever the name of the column, that is
in camle case (firstName) or with _ (first_name) eloquent know to
match to it. The format of the accessor should be
get[NameOfATtribute]Attribute in pascal case (camle case but first
letter too in uppercase).
Three the method argument hold the value of the column in
question. Bellow a snippet that show how it's used
$user = App\User::find(1);
$firstName = $user->first_name; //|=> first_name => getFirstNameAttribute(columnVal)
The resolution is clear.
first_name (column name) => getFirstNameAttribute(columnValue)
All the snippets are from the doc: https://laravel.com/docs/5.8/eloquent-mutators#accessors-and-mutators
 Let's apply all of that
First we need to not use $table->timestamps() in the migration so we make the changment to the bellow.
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
Then we apply the modification on our model:
- we deactivate the auto handling of timestamps.
- Override the timestamps columns names.
- And create the accessors.
Here depend on what we want. If we want to only do the above here a snippet that show that:
// deactivate auto timestamps management
public $timestamps = false;
// change the columns names
const CREATED_AT = 'car_time';
const UPDATED_AT = 'cardata_time';
// creating the accessors (respect the naming)
protected function getCarTimeAttribute($value) //car_time => CarTime
{
// <-- do whatever you want here (example bellow)
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataTimeAttribute($value) //cardata_time => CardataTime
{
// <-- do whatever you want here
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Doing it with renaming the attributes completely
If what you want is to use another accessing name. Then what my friend #caddy dz did is the way to go. Which happen to be sitting with me. And dared me to expand upon the answer. (hhhh)
You will need to know
$appends and $hidden
Part of the serialization API.
https://laravel.com/docs/master/eloquent-serialization#appending-values-to-json
https://laravel.com/docs/master/eloquent-serialization#hiding-attributes-from-json
$appends allow us to add attributes to the model. That don't exists on the table. We need also to create an accessors for them.
class User extends Model
{
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['is_admin'];
// ........
/**
* Get the administrator flag for the user.
*
* #return bool
*/
public function getIsAdminAttribute()
{
return $this->attributes['admin'] == 'yes';
}
}
and
$hidden allow us to remove and limit the attribute from the models. Like with the password field.
Doc examples:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['password'];
}
And from that what we need to do is to hide the attributes holding the time, that want to be changed to somehting else.
// remove the old attributes names
protected $hidden = ['car_time', 'cardata_time']; // renaming those
// append the new one \/ \/ <- to those
protected $appends = ['car_crated_at', 'cardata_created_at']; // names just for illustration
protected function getCarCreatedAtAttribute($value) // car_created_at => CarCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getCardataCreatedAtAttribute($value) // cardata_created_at => CardataCreatedAt
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
 Applying it for different models
The basic idea is to create a base model then extend it when you create your model.
Formatting all time attributes of the model without exception
If what you want is to apply the formatting for all the time attributes within the model.
Then override serializeDate() method. In practice write a trait, and then you can apply it. Otherwise a base model.
The answer bellow cover it well:
https://stackoverflow.com/a/41569026/7668448
And historically This thread is interesting :
https://github.com/laravel/framework/issues/21703
Serializing in Carbon level
In the documentation laravel 5.7 and up (what i checked [doc only]) :
https://laravel.com/docs/master/eloquent-serialization#date-serialization
We can change the formatting at the level of carbon serialization. But it happen that there was a bug in the past. Normally fixed but i didn't try it. Bug was in 5.7 and fixed in 5.7 if i'm not wrong. The git link above discuss it.
Snippet:
class AppServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format('U');
});
}
___THE_END ^ ^
Not sure what you're asking but if you have cardata_time and car_time in your table defined like this
Schema::create('cars', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamp('cardata_time', 0)->nullable();
$table->timestamp('car_time', 0)->nullable();
});
And a MasterModel like so
/**
* Indicates if the model should be timestamped.
*
* #var bool
*/
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = ['created_at', 'updated_at'];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['car_time', 'cardata_time'];
protected function getCreatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
protected function getUpdatedAtAttribute($value)
{
$format = "Y-m-d\TH:i:s\Z";
$datetime = new DateTime($value);
return $datetime->format($format);
}
Results:
{
"id": 1,
"created_at": "2019-09-02T20:31:38Z",
"updated_at": "2019-09-02T20:31:38Z"
}
As in the documentation. The first approach. This requires the dates, to be defined in the $dates property. This will only be triggered if the Model is serialized.
public class YourModel extends Model
{
protected $dateFormat = "Y-m-d\TH:i:s\Z";
}
You can also define it in a provider in the boot method. Which will trigger when a Carbon date is serialized.
public function boot()
{
Carbon::serializeUsing(function ($carbon) {
return $carbon->format("Y-m-d\TH:i:s\Z");
});
}

valid constraint does not support group options in symfony2

I need to cascade validation in symfony2 form unless for specified group.
here symfony team told that group option is not supported in Valid constraint
https://github.com/symfony/symfony/issues/4893
How to do it ?
Details:
I have a User Entity has address property which is a foreign key to Address Entity. Also i have Entity called business having User as property and also Address property. I need to validate address for User but, without validating it When User is a property of Business...
Schema
Class Address {
...
}
Class User {
/**
* #Assert\Valid(groups={"user"})
*/
private $address;
}
Class Business {
/**
* #Assert\Valid(groups={"business"})
*/
private $user;
/**
* #Assert\Valid(groups={"business"})
*/
private $address;
}
So I need to validate The address inside User only for User Forms but not for Business.
Thank you
I was faced with the same problem (Symfony 3).
I have a entity UserInfo with two fields linked to one entity Place.
And I need to validate both fields in one case, and one field in another case.
And didn't want to move constraints into Form.
In first atemt I used a Callback constraint to check group and validate one or both fields. It was fine. But input fields in form wasn't marked as invalid. All errors was displayed at top of the form.
Then I simply created own validator. Thanks to this I can specify needed groups for each field. And all invalid input fields in form marked accordingly.
/**
* #Annotation
* #Target({"PROPERTY", "METHOD", "ANNOTATION"})
*/
class ValidGroupAware extends Constraint
{
}
class ValidGroupAwareValidator extends ConstraintValidator
{
/**
* Checks if the passed value is valid.
*
* #param mixed $value The value that should be validated
* #param Constraint $constraint The constraint for the validation
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidGroupAware) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\ValidGroupAware');
}
$violations = $this->context->getValidator()->validate($value, [new Valid()], [$this->context->getGroup()]);
/** #var ConstraintViolation[] $violations */
foreach ($violations as $violation) {
$this->context->buildViolation($violation->getMessage())
->setParameters($violation->getParameters())
->setCode($violation->getCode())
->setCause($violation->getCause())
->setPlural($violation->getPlural())
->setInvalidValue($violation->getInvalidValue())
->atPath($violation->getPropertyPath())
->addViolation();
}
}
}
Ok, I have a solution. Callback constraints do have 'groups' option and we can use it here. In the callback we will call validation for the required entity.
I will use php code for adding constraints, but you could use annotations.
User entity
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addConstraint(new Assert\Callback(array(
'methods' => array('validAddress'),
'groups' => array('user'),
)));
}
public function validAddress(ExecutionContextInterface $context)
{
$context->validate($this->address, 'address', $context->getGroup());
}
Business entity
// ...
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addConstraint(new Assert\Callback(array(
'methods' => array('validUser'),
'groups' => array('business'),
)));
$metadata->addConstraint(new Assert\Callback(array(
'methods' => array('validAddress'),
'groups' => array('business'),
)));
}
public function validUser(ExecutionContextInterface $context)
{
$context->validate($this->user, 'user', $context->getGroup());
}
public function validAddress(ExecutionContextInterface $context)
{
$context->validate($this->address, 'address', $context->getGroup());
}
PS Of course my code can be optimized
You can add your validation rules in your User FormType.
Don't forget to delete your annotation.
Follow this link to learn more about validation in form type

doctrine 2, how to remove many to many associations?

How do you unlink a relation from a many-to-many table without deleting anything?
I have tried:
$getProject = $this->_helper->getDocRepo('Entities\Project')->findOneBy(array('id' => $projectId));
$getCat = $this->_doctrine->getReference('\Entities\Projectcat', $catId);
$getProject->getCategory()->removeElement($getCat);
$this->em->flush();
my Projectcat entity:
/**
* #ManyToMany(targetEntity="\Entities\Projectcat", cascade={"persist", "remove"})
* #JoinColumn(name="id", referencedColumnName="id")
*/
protected $getCategory;
A rather old post but wanted to provide a way to ensure the association was removed from the ORM Entity side of doctrine, rather than of having to manually execute each Entity's collection removeElement and to expand on the answer by #Rene Terstegen.
The issue is that Doctrine does not "auto-magically" tie together the associations, you can however update the entity's Add/Remove methods to do so.
https://gist.github.com/Ocramius/3121916
The below example is based on the OP's project/category schema.
It assumes that the table project_category is the ManyToMany relationship table, and the project and category tables use the primary key id.
class Project
{
/**
* #ORM\ManyToMany(targetEntity="Category", inversedBy="projects")
* #ORM\JoinTable(
* name="project_category",
* joinColumns={
* #ORM\JoinColumn(name="project", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="category", referencedColumnName="id")
* }
* )
*/
protected $categories;
public function __construct()
{
$this->categories = new ArrayCollection();
}
/**
* #param Category $category
*/
public function removeCategory(Category $category)
{
if (!$this->categories->contains($category)) {
return;
}
$this->categories->removeElement($category);
$category->removeProject($this);
}
}
class Category
{
/**
* #ORM\ManyToMany(targetEntity="Project", mappedBy="categories")
*/
protected $projects;
public function __construct()
{
$this->projects = new ArrayCollection();
}
/**
* #param Project $project
*/
public function removeProject(Project $project)
{
if (!$this->projects->contains($project)) {
return;
}
$this->projects->removeElement($project);
$project->removeCategory($this);
}
}
Then all you need to do is to call the removeCategory or removeProject method, instead of both. The same can be applied for addCategory and addProject methods as well.
$project = $em->find('Entities\Project', $projectId);
$category = $em->getReference('Entities\Category', $categoryId);
$project->removeCategory($category);
$em->flush();
Your information is a bit limited. Some extra information about the database scheme and Project would be nice. But to give it a try.
You have to remove it from both sides of the relationship. You removed it from the category, but you should also remove it from the project.
// Remove Category from Project
$Project->Category->removeElement($Category);
// Remove Project from Category
$Category->Project->removeElement($Project);
Good luck!
An old post, but the answer above helped me, but it may help to expand it a little bit, I have a project that can have many categories (and categories than can have many projects), so this code gets me all of them:
$project->getCategories();
If I wanted to delete all of the categories for a project, I simply do this:
foreach ($project->getCategories() as $category) {
$project->getCategories()->removeElement($category);
}
The issue with the original question was that I believe Doctrine wants you to pass in the category that is referenced by the project, not just a reference to the category that you grabbed independently using this code:
$getCat = $this->_doctrine->getReference('\Entities\Projectcat', $catId);
Hopefully that makes sense. I know I'm messing up the terminology slightly.

Resources