Symfony - softdeleteable delete the entity but let me query it after - api

I want to implement softdelete in my symfony application.
I'm using Symfony5 and ApiPlatform for the back.
I've run the following command to install the bundle
composer require stof/doctrine-extensions-bundle
Installation worked as composer.json now has :
"stof/doctrine-extensions-bundle": "^1.5",
So I've updated my stof_doctrine_extensions.yaml file as follow:
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
softdeleteable: true
I added the #Gedmo annotation to the entity I want to apply softdeleteable on
Added the deletedAt field, generated the migration and reload the database.
Here is my Entity.php file:
/**
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=false)
* #ORM\Entity(repositoryClass=EntityRepository::class)
*/
class Entity
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"entity:read", "entity:list", "user:read", "user:list"})
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups({"entity:write", "entity:read", "entity:list"})
*/
protected $firstName;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $deletedAt;
}
I've also written a simple test to see if the feature was working:
public function testRoleAdminCanSoftDeleteEntity(): void
{
$this->buildEntityManager();
$res = $this->buildDeleteRequest(
GenericRoutes::ROUTE.'/11',
GenericCredentials::ADMIN_CREDENTIALS
);
$entity = $this->em->getRepository(Entity::class)->findEntityByEntityField(json_decode(GenericCredentials::CREDENTIALS)->email);
$this->assertNull($entity);
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(204);
}
I receive a 204 but the specialist isn't empty.
I've also used the swagger to manually delete the entity/{id} I wanted to remove.
I also receive a 204 and then can query the same entity/{id} right after.
I've verified in the database and the deleted_at field is updated to the moment I ran the DELETE request.
Does anyone has any idea why I still can query this user which I just soft deleted ?

So, regarding to my comment.
You just need to configure filter in the doctrine config, smth like this:
doctrine:
dbal:
...
orm:
...
filters:
softdeleteable:
class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
enabled: true
But in my opinion, it would be much more easy to not using this extension, it is just one more dependency in your project, and just create yourself nullable deletedAt property to the entity and set it to DateTime('now') on delete, and then you can get not deleted entries from the repo just adding one more criteria to findBy - 'deletedAt' => null

Related

JMSSerializer Bundle - Circular Reference Error (Only on Prod Azure Environment) - Symfony4/Doctrine2 REST API

So I know somewhat similar issues have been discussed numerous times before but I haven't had any luck finding a solution with this specific issue.
Running locally (using MAMP) I have no issues with my API responses. However once deployed to the production Azure server (via Ansible) I run into the dreaded error:
request.CRITICAL: Uncaught PHP Exception Symfony\Component\Serializer\Exception\CircularReferenceException: "A circular reference has been detected when serializing the object of class "App\ServiceProviderBundle\Entity\Plan
I'm confident that all of my doctrine associations are setup correctly yet something is triggering an infinite loop.
Here is a simplified entity relationship and the main associations from within my doctrine classes.
Any comments or help would be greatly suggested - many thanks!
Plan -> (hasMany) Bundle -> (hasMany) -> Product
class Plan {
/**
* #ORM\OneToMany(targetEntity="App\ServiceProviderBundle\Entity\Bundle", mappedBy="plan")
*/
private $bundles;
}
class Bundle {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Plan", inversedBy="bundles")
* #ORM\JoinColumn(nullable=true)
*/
private $plan;
/**
* #SerializedName("products")
* #ORM\OneToMany(targetEntity="App\ServiceProviderBundle\Entity\BundleProduct", mappedBy="bundle",
* cascade={"persist", "remove"})
* #ORM\JoinColumn(nullable=false)
*/
private $bundleProducts;
}
class BundleProduct {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Bundle", inversedBy="bundleProducts")
* #ORM\JoinColumn(nullable=false)
*/
private $bundle;
}
Use #Exclude annotation like that:
class BundleProduct {
/**
* #ORM\ManyToOne(targetEntity="App\ServiceProviderBundle\Entity\Bundle", inversedBy="bundleProducts")
* #ORM\JoinColumn(nullable=false)
* #Exclude
*/
private $bundle;
}

Symfony3 Profiler Storage

in the Docs
http://symfony.com/doc/master/cookbook/profiler/storage.html
you still can find Information about Profiler Storage.
I just checked the code and could not find any clues how to set a custom storage.
I also find no Documentation stating this except some #legacy notes in the Original Source at 2.8.
Is there a Reason why this was removed?
I was using redis to store this data with a lifetime of eta 1hour.
Now I need to run a manual cleanup to whipe all files in that directory.
If anybody has some clues or hints on helping me with this issue are appreceated ^^
Chris
Thanks to the Tip of Matteo I was able to solve this quite flexible.
The Team of Symfony removed this, because it was hard coded into the Profiler Subsystem.
Instead of fixing this by adding a class parameter I had to solve it. :)
Ok, here is the code, If somebody needs this to.
First of all we need the Original Classes from Symfony 2.7 (at least I reused them as I only need the Redis Option ( I use it, because I can Compress the data using igbinary)
Next you need to implement a Compiler Pass.
namespace AcmeBunlde\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ProfilerCompilerPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* #param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('profiler');
$definition->addArgument('%acmebundle.profiler.defaultEnabled%');
$definition->addArgument('%acmebundle.profiler.class%');
$definition->addArgument('%acmebundle.profiler.dsn%');
$definition->addArgument('%acmebundle.profiler.username%');
$definition->addArgument('%acmebundle.profiler.password%');
$definition->addArgument('%acmebundle.profiler.ttl%');
$definition->setClass('acmebundle\Profiler\Profiler');
}
}
This needs to be loaded inside the Bundle Loader:
public function build(ContainerBuilder $container)
{
...
$container->addCompilerPass(new ProfilerCompilerPass());
}
After this we need to add the Configuration for the New Profiler Storage in the DependencyInjection Folder.
namespace AcmeBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
* #author Chris
*/
class Configuration implements ConfigurationInterface
/**
* {#inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('library');
$rootNode
->children()
->arrayNode('profiler')
->addDefaultsIfNotSet()
->children()
->booleanNode('defaultStorage')
->defaultTrue()
->end()
->scalarNode('class')
->defaultValue('')
->end()
->scalarNode('dsn')
->defaultValue('')
->end()
->scalarNode('username')
->defaultValue('')
->end()
->scalarNode('password')
->defaultValue('')
->end()
->scalarNode('ttl')
->defaultValue('3600')
->end()
->end()
->end();
return $treeBuilder();
}
}
Now set the Default Values in The Dependency Injection Bundle Loader
<?php
namespace AcmeBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
* #author Chris
*/
class AcmeExtension extends Extension
{
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
...
$container->setParameter('acmebundle.profiler.defaultEnabled',$config['profiler']['defaultStorage']);
$container->setParameter('acmebundle.profiler.class',$config['profiler']['class']);
$container->setParameter('acmebundle.profiler.dsn',$config['profiler']['dsn']);
$container->setParameter('acmebundle.profiler.username',$config['profiler']['username']);
$container->setParameter('acmebundle.profiler.password',$config['profiler']['password']);
$container->setParameter('acmebundle.profiler.ttl',$config['profiler']['ttl']);
...
}
...
}
As Last Step you need to build a basic container for adding the new Profiler Handler.
I have choosen to implement it not to complex:
<?php
namespace AcmeBundle\Profiler;
use Psr\Log\LoggerInterface;
use \Symfony\Component\HttpKernel\Profiler\Profiler as ProfilerSrc;
use Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface;
/**
* Profiler.
*/
class Profiler extends ProfilerSrc
{
public function __construct(ProfilerStorageInterface $storage, LoggerInterface $logger, $defaultEnabled=true,$class=null,$dsn=null,$username=null,$password=null,$ttl=3600)
{
if($defaultEnabled!==true)
{
$storage = new $class($dsn,$username,$password,$ttl);
}
parent::__construct($storage , $logger);
}
}
I have also added a Library to define the Constructor of the Storage Interface.
<?php
namespace AcmeBundle\Profiler;
use Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface as ProfilerStorageSource;
interface ProfilerStorageInterface extends ProfilerStorageSource
{
/**
* ProfilerStorageInterface constructor.
*
* #param $dsn
* #param $username
* #param $password
* #param $ttl
*/
public function __construct($dsn,$username,$password,$ttl);
}
All you need to do now is to define some Options in your config_dev.yml file.
acmebundle:
profiler:
defaultEnabled: false
class:CLASSNAME INCLUDING NAMESPACE
dsn: redis://localhost/1
username:
password
ttl: 3600
with defaultEnabled = true you can reenable to Original Handler.
the rest is, I believe self explaining.
username + password is from the original feature set.
(ttl == lifetime)
I hope this helps somebody else as well :)
Is marked as deprecated since 2.8 with the suppression in the 3.0. I can't find any motivation about in the PR. The doc is not yet updated as you mention.
The only suggestion is about a comment in this issue:
If you want to use your own implementation of a profiler storage,
then just override the profile.storage service.
Hope this help

Tx_Extbase_Domain_Repository_FrontendUserRepository->findAll() not working in typo3 4.5.30?

I am trying to run a simple query off of the Tx_Extbase_Domain_Repository_FrontendUserRepository. I cannot get anything to work except findByUid(), not even findAll().
In my controller I have this code which seems to work:
/**
* #var Tx_Extbase_Domain_Repository_FrontendUserRepository
*/
protected $userRepository;
/**
* Inject the user repository
* #param Tx_Extbase_Domain_Repository_FrontendUserRepository $userRepository
* #return void */
public function injectFrontendUserRepository(Tx_Extbase_Domain_Repository_FrontendUserRepository $userRepository) {
$this->userRepository = $userRepository;
}
/**
* action create
*
* #param Tx_BpsCoupons_Domain_Model_Coupon $newCoupon
* #return void
*/
public function createAction(Tx_BpsCoupons_Domain_Model_Coupon $newCoupon) {
...... some code .....
$user = $this->userRepository->findByUid(($GLOBALS['TSFE']->fe_user->user[uid]));
$newCoupon->setCreator($user);
...... some code .....
}
but in another function I want to look up a user not by uid but by a fe_users column called vipnumber (an int column) so I tried
/**
* check to see if there is already a user with this vip number in the database
* #param string $vip
* #return bool
*/
public function isVipValid($vip) {
echo "<br/>" . __FUNCTION__ . __LINE__ . "<br/>";
echo "<br/>".$vip."<br/>";
//$ret = $this->userRepository->findByUid(15); //this works!! but
$query = $this->userRepository->createQuery();
$query->matching($query->equals('vip',$vip) );
$ret = $query->execute(); //no luck
.................
and neither does this
$ret = $this->userRepository->findAll();
How can one work but not the others? In my setup I already put
config.tx_extbase.persistence.classes.Tx_Extbase_Domain_Model_FrontendUser.mapping.recordType >
which seems to be necessary for the fiondByUid to work, is i t preventing the other from working?
I am using typo3 v 4.5.30 with extbase 1.3
Thanks
If $this->userRepository->findByUid(15); works, there is no reason why $this->userRepository->findAll(); should not. However $this->userRepository->findAll(); returns not a single Object but a collection of all objects, so you have to iterate over them.
If you add a column to the fe_users, you have to add it to TCA and to your extbase model (you need a getter and a setter), too! After that you can call findByProperty($property) in your repository. In your case that would be
$user = $this->userRepository->findByVipnumber($vip);
This will return all UserObjects that have $vip set as their Vipnumber. If you just want to check if that $vip is already in use, you can call
$user = $this->userRepository->countByVipnumber($vip);
instead. Which obviously returns the number of Users that have this $vip;
You never use $query = $this->createQuery(); outside your Repository.
To add the property to the fronenduser Model you create your own model Classes/Domain/Model/FronendUser.php:
class Tx_MyExt_Domain_Model_FrontendUser extends Tx_Extbase_Domain_Model_FrontendUser {
/**
* #var string/integer
*/
protected $vipnumber;
}
Add a getter and a setter. Now you create your own FrontendUserRepository and extend the extbase one like you did with the model. You use this repository in your Controller. Now you're almost there: Tell Extbase via typoscript, that your model is using the fe_users table and everything should work:
config.tx_extbase {
persistence{
Tx_MyExt_Domain_Model_FrontendUser{
mapping {
tableName = fe_users
}
}
}
}
To disable storagePids in your repository in general, you can use this code inside your repository:
/**
* sets query settings repository-wide
*
* #return void
*/
public function initializeObject() {
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings->setRespectStoragePage(FALSE);
$this->setDefaultQuerySettings($querySettings);
}
After this, your Querys will work for all PIDs.
I didn't have the opportunity to work with frontend users yet, so I don't know if the following applies in this case:
In a custom table I stumbled uppon the fact, that extbase repositories automatically have a look at the pids stored in each entry and check it against a set storage pid (possibly also the current pid if not set). Searching for a uid usually means you have a specific dataset in mind so automatic checks for other values could logically be ignored which would support your experiences. I'd try to set the storage pid for your extension to the place the frontend users are stored in ts-setup:
plugin.[replace_with_extkey].persistence.storagePid = [replace_with_pid]

ORM relation mapping issues in Doctrine and ZF2

I was puzzled by Doctrine\ORM\Mapping issue. I have two entities and they are Many-To-One, Unidirectional releationship. I want to operate Chipinfo(add/update/delete) without impact on producer. Only persist Chipinfo while no persist producer..
class Chipinfo implements
{
/**
* #var integer
*
* #ORM\Column(name="ChipID", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $chipid;
/**
* #var \Shop\Entity\Producer
*
* #ORM\ManyToOne(targetEntity="Shop\Entity\Producer")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="pId", referencedColumnName="producerid")
* })
*/
protected $pid;
}
class Producer{
/**
* #ORM\Id
* #ORM\Column(name="producerid", type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $producerId;
/**
* #ORM\Column(name="producername", type="string")
*/
protected $producerName;
}
ChipInfo and Producer are Many-To-One unidirection relationship: A chipinfo can only be built by one producer while one producer can build multiple chipinfos. What I want is that: add/update/removal of items in Chipinfo would not do any impact on Producer.
$chip = new Chipinfo();
$formData = $this->initFormData($form->getData());
$chip->populate($formData);
$this->getEntityManager()->persist($chip);
$this->getEntityManager()->flush();
private function initFormData(&$raw){
$raw['eid'] = new Encapuser($this->findEntity("Shop\Entity\Encapuser", $raw['eid']));
$this->log($raw->eid);
$raw['vid'] = new Vendors($this->findEntity("Shop\Entity\Vendors", $raw['vid']));
$raw['pid'] = new Producer($this->findEntity("Shop\Entity\Producer", $raw['pid']));
$this->log($raw);
return $raw;
}
would throws errors:
A new entity was found through the relationship 'Shop\Entity\Chipinfo#pid' that was not configured to cascade persist operations for entity: Shop\Entity\Producer#00000000349002ee00000000c955fd11. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Shop\Entity\Producer#__toString()' to get a clue.
Then I configured the pid as:
#ORM\ManyToOne(targetEntity="Shop\Entity\Producer", cascade={"persist"})
Though the error disappear, but this is not what I want. Because when i call flush() for chipinfo with existing producer A, a new A which is duplicated is inserted.
Therefore, my questions are:
1) How should i configure #manyToone field, I did not get clue from http://doctrine-orm.readthedocs.org/en/2.0.x/reference/working-with-associations.html#transitive-persistence-cascade-operations
2) Should I add:
#ORM\OneToMany, targetEntity="Shop\Entity\Producer"
private #chip;
in producer? If so, would operations(add/delete/update) on producer require construct a #chip?
You have to set the existing producer (the entity retrieved by doctrine) in the $pid field of your entity.
Why do you create a new Producer ? What does findEntity do ? It does not seem to retrieve the actual entity from Doctrine, why is that ?
Usually what you would do is this :
$chip->setPid($this->getEntityManager()->getRepository('Shop\Entity\Producer')->find($pid));

Doctrine2 ArrayCollection

Ok, I have a User entity as follows
<?php
class User
{
/**
* #var integer
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #var \Application\Entity\Url[]
* #OneToMany(targetEntity="Url", mappedBy="user", cascade={"persist", "remove"})
*/
protected $urls;
public function __construct()
{
$this->urls = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addUrl($url)
{
// This is where I have a problem
}
}
Now, what I want to do is check if the User has already the $url in the $urls ArrayCollection before persisting the $url.
Now some of the examples I found says we should do something like
if (!$this->getUrls()->contains($url)) {
// add url
}
but this doesn't work as this compares the element values. As the $url doesn't have id value yet, this will always fail and $url will be dublicated.
So I'd really appreciate if someone could explain how I can add an element to the ArrayCollection without persisting it and avoiding the duplication?
Edit
I have managed to achive this via
$p = function ($key, $element) use ($url)
{
if ($element->getUrlHash() == $url->getUrlHash()) {
return true;
} else {
return false;
}
};
But doesn't this still load all urls and then performs the check? I don't think this is efficient as there might be thousands of urls per user.
This is not yet possible in a "domain driven" way, ie. just using objects. You should execute a query to check for the existance:
SELECT count(u.id) FROM User u WHERE ?1 IN u.urls AND u.id = ?2
With Doctrine 2.1 this will be possible using a combination of two new features:
Extra Lazy Collections
#IndexBy for collections, so you would define #OneToMany(targetEntity="Url", indexBy="location")
ExtraLazy Collection Support for index by using ->contains().
Points 1 and 2 are already implemented in Doctrine 2 master, but 3 is still missing.
You should try using the exists method on the collection and manually compare values.