Doctrine return null in place of EntityNotFoundException - php

I have broken FKs in my database and if I load an entity and ask for a related entity Doctrine will throw \Doctrine\ORM\EntityNotFoundException.
For the entity in question, I would prefer that where the FK is broken it would return NULL rather than throw an exception. This is because its within a Twig template that the exception occurs and I would prefer Twig to not have to have to handle the exception in this case.
The following is an example configuration.
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Foo\Click" table="clicks">
<id name="id" type="bigint" column="click_id">
<generator strategy="IDENTITY"/>
</id>
<!-- .. -->
<many-to-one field="visitor" target-entity="Foo\Visitor" fetch="LAZY">
<join-columns>
<join-column name="visitor_id" referenced-column-name="visitor_id"/>
</join-columns>
</many-to-one>
</entity>
<entity name="Foo\Visitor" table="visitors" read-only="true">
<id name="visitorId" type="integer" column="visitor_id">
<generator strategy="IDENTITY"/>
</id>
<!-- ... -->
<one-to-one field="firstClick" target-entity="Foo\Click" fetch="LAZY">
<join-columns>
<join-column name="click_id" referenced-column-name="click_id"/>
</join-columns>
</one-to-one>
</entity>
</doctrine-mapping>
The following is an example of expected results where the the click as a visitor ID, but the a visitor record does not exists with that ID. In this case, I would rather not have to wrap the logic in Try/Catch and instead have Click::getVisitor() return null;
<?php
$clickOne = $entityManager()->find(Foo\Click::class, 1);
$v = $clickOne->getVisitor();
if ($v !== null) {
echo $v->getId(); // may throw Doctrine\ORM\EntityNotFoundException
}
Is there a strategy for this with Doctrine?
Update: Added example configuration and code, and now I see the why this is not achievable with a simple Doctrine configuration.

EntityNotFoundException is thrown from Doctrine's proxy. So you can use EAGER loading method to get rid of proxies and get NULL instead of exception.

This is the strategy I have adopted based on the comment made by iainn.
<?php
class Parent
{
protected $child;
public function getChild()
{
if ($this->child instance of \Doctrine\ORM\Proxy\Proxy) {
try {
$this->child->__load();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
$this->child = null
}
}
return $this->child;
}
}
I am sure it is not recommended for your entities to interact directly with proxies. But I preferred this over calling a known method on the entity (which would also have the effect of loading it) because the intention of this code is clearer to the next developer who might read it.
I'm not sure if there are any side effects with interacting with the proxy like this.

Related

Array-Properties with the SeralizedPath-Attribute can't be serialized by Serializer

I have the problem that Properties with SerializedPath attributes, which I convert to an XML via Symfony Serializer (SerializerInterface, symfony/serializer-pack), does not have an expected behavior with arrays. Instead, the AbstractObjectNormalizer throws an exception. Symfony and related packages are using v6.2.5.
My goal is to create an XML like this one, with e.g. the following Object:
<?xml version="1.0"?>
<response>
<payment>
<allowedMethods>
<include name="Method A"/>
<include name="Method B"/>
</allowedMethods>
</payment>
</response>
// This is not working
class Payment
{
#[SerializedPath('[payment][allowedMethods][include][#name]')]
public array $methods = ['Method A', 'Method B'];
}
// ...
$this->serializer->serialize(new Payment(), 'xml');
But it results to the following Exception: The element you are trying to set is already populated: "[payment][allowedMethods][include][#name]".
which is thrown in the AbstractObjectNormalizer of Symfony:
if (null !== $classMetadata && null !== $serializedPath = ($attributesMetadata[$attribute] ?? null)?->getSerializedPath()) {
$propertyAccessor = PropertyAccess::createPropertyAccessor();
if ($propertyAccessor->isReadable($data, $serializedPath) && null !== $propertyAccessor->getValue($data, $serializedPath)) {
throw new LogicException(sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath));
}
$propertyAccessor->setValue($data, $serializedPath, $attributeValue);
return $data;
}
My expected behavior would be that it behaves identically to the scalar values and therefore recognizes the # as an XML attribute and the rest as nodes:
// This is working great!
class Payment
{
#[SerializedPath('[payment][usedMethod][#name]')]
public string $usedMethod = 'Method A';
}
<?xml version="1.0"?>
<response>
<payment>
<usedMethod name="Method A"/>
</payment>
</response>
I've been trying to identify the exact problem and find a solution for some time, but since SerializedPath is very new (since Symfony 6.2), there is no great documentation for it. Internally it uses the PropertyAccessor, but where I could not find a suitable solution either.
Can someone maybe explain me the problem in more detail or even know a solution? Or even an alternative way. I'm trying to do it via the SerializedPath to avoid all the nested DTOs that are just there to map the (not always plausible) data model of the XML.
Another quite interesting fact is, if you comment out the thrown Exception in the AbstractObjectNormalizer it kind of works (for json fully, for xml partially, because the array can only hold one entry because of the annotation key):
class Payment
{
#[SerializedPath('[payment][allowedMethods][include]')]
public array $methods = [
'#name' => 'Method A'
];
}
<?xml version="1.0"?>
<response>
<payment>
<allowedMethods>
<include name="Method A"/>
</allowedMethods>
</payment>
</response>

Sulu CMS - Visiblity condition from underlying value of resource single selection

I have three entities Provision, Gatekeeper and Data.
class Provision {
private Gatekeeper $keeper; // ManyToOne
private Collection $points; // ManyToMany
}
class Gatekeeper {
private int $id;
private string $name;
private bool $allowData;
}
class Data {
private int $value;
}
All entities have REST-CRUD logic implemented. In order to edit the Provision entity I've added configurations to the sulu_admin.yaml:
sulu_admin:
field_type_options:
selection:
data_selection:
default_type: 'list_overlay'
resource_key: 'data'
types:
list_overlay:
adapter: 'table'
list_key: 'data'
display_properties: ['value']
icon: 'su-plus'
label: 'app.data'
overlay_title: 'app.action.select.data'
single_selection:
single_gatekeeper_selection:
default_type: 'single_select'
resource_key: 'gatekeepers'
types:
single_select:
display_property: 'name'
id_property: 'id'
overlay_title: 'app.action.select.gatekeeper'
Now, in the form configuration file for Provision entities, I want to include a visibleCondition for the data list:
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/form-1.0.xsd">
<key>provision_details</key>
<properties>
<property name="gatekeeper" type="single_gatekeeper_selection" mandatory="true">
<meta>
<title>app.gatekeeper</title>
</meta>
</property>
<property name="data" type="data_selection" visibleCondition="!!gatekeeper && gatekeeper.allowData">
<meta>
<title>app.data</title>
</meta>
</property>
</properties>
</form>
Is there any possibility to implement this behaviour? I've already tried adding a custom ConditionDataProvider, but the main problem is that the visibleCondition is evaluated at Field-level while the only place I could pull the required data from would be the ResourceListStore in the SingleSelect.
This is not possible you only have access to the raw form data. For a single selection the gatekeeper value contains only the id of the selected element. Not any data of the element behind this element.
All data you have access to it, you see in save (POST/PUT) request of the form.
The only thing which is possible would be using resource_store_properties_to_request to filter the result of your data_selection by the gatekeeper id, this requires that your data api need to keep the gatekeeper parameter in mind to filter by it:
<properties>
<property name="gatekeeper" type="single_gatekeeper_selection" mandatory="true">
<meta>
<title>app.gatekeeper</title>
</meta>
</property>
<property name="data" type="data_selection">
<meta>
<title>app.data</title>
</meta>
<params>
<param name="resource_store_properties_to_request" type="collection">
<param name="gatekeeper" value="gatekeeper"/>
</param>
</params>
</property>
</properties>

Sulu CMF - Pass parameter to autocomplete field in FormOverlayList add form

This question is a followup.
I have two entities DataSet and DataGroup.
class DataSet {
string $name;
Collection $groups; // Collection<int, DataGroup>
}
class DataGroup {
string $name;
DataSet $dataSet;
?DataGroup $nextGroup; // Condition: $nextGroup !== $this && $nextGroup->dataSet === $this->dataSet
}
The property DataGroup::nextGroup may refer to any other DataGroup entity associated with the same DataSet.
I want to create a CRUD form where I can add, edit and remove DataSet entities. In this DataSet form, I also want to include a tab where I can CRUD DataGroup entities associated with the current DataSet.
I have created list metadata data_sets.xml and data_groups.xml, as well as form metadata data_set.xml and data_group.xml.
<!-- lists/data_sets.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_sets</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
</properties>
</list>
<!-- lists/data_groups.xml -->
<list xmlns="http://schemas.sulu.io/list-builder/list">
<key>data_groups</key>
<properties>
<property name="name" visibility="always" searchability="yes">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
</property>
<property name="dataSet" visibility="always">
<field-name>name</field-name>
<entity-name>App\Entity\DataSet</entity-name>
<joins>
<join>
<entity-name>App\Entity\DataSet</entity-name>
<field-name>App\Entity\DataGroup.dataSet</field-name>
</join>
</joins>
</property>
</properties>
</list>
<!-- forms/data_set.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_set</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
</properties>
</form>
<!-- forms/data_group.xml -->
<form xmlns="http://schemas.sulu.io/template/template"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://schemas.sulu.io/template/template
http://schemas.sulu.io/template/form-1.0.xsd"
>
<key>data_group</key>
<properties>
<property name="name" type="text_line" mandatory="true">
<params>
<param name="headline" value="true"/>
</params>
</property>
<property name="nextGroup" type="single_data_group_selection">
<params>
<param name="type" value="auto_complete"/>
<param name="resource_store_properties_to_request" type="collection">
<param name="dataSetId" value="id"/>
</param>
</params>
</property>
</properties>
</form>
I have also configured sulu_admin.yaml for REST routes and the custom single selection:
sulu_admin:
resources:
data_sets:
routes:
list: app.get_data_sets
detail: app.get_data_set
data_groups:
routes:
list: app.get_data_groups
details: app.get_data_group
field_type_options:
single_selection:
single_data_group_selection:
default_type: 'auto_complete'
resource_key: 'data_groups'
types:
auto_complete:
display_property: 'name'
search_properties:
- 'name'
I implemented two REST controllers for both entities. DataSetController is built as of the Docs, while DataGroupController has a small extension for the list route:
class DataGroupController implements ClassResourceInterface
{
public function cgetAction(int $dataSetId, Request $request): Response
{
// ... Init field descriptors and execute listBuilder
$list = new ListRepresentation(
$listResponse,
'data_groups',
\array_merge(['dataSetId' => $dataSetId], $request->query->all()), // add DataSet ID
$listBuilder->getCurrentPage(),
$listBuilder->getLimit(),
$listBuilder->count()
);
// ... handle view
}
// ...
}
Finally, I implemented a custom Admin class like explained in the Docs.
class DataAdmin extends Admin
{
public const DATA_SET_DETAILS = 'data_set_details';
public const DATA_SET_GROUPS = 'data_set_groups';
public const DATA_SET_LIST = 'app.data_sets_list';
public const DATA_SET_ADD_FORM = 'app.data_set_add_form';
public const DATA_SET_ADD_FORM_DETAILS = 'app.data_set_add_form.details';
public const DATA_SET_EDIT_FORM = 'app.data_set_edit_form';
public const DATA_SET_EDIT_FORM_DETAILS = 'app.data_set_edit_form.details';
public const DATA_SET_EDIT_FORM_GROUPS = 'app.data_set_edit_form.groups';
public function configureViews(ViewCollection $viewCollection): void
{
// Add DataSet list view
// Add DataSet add form view
// Add DataSet edit form view (details)
/*
* Custom second DataSet edit form tab for DataGroup CRUDding
*/
$groupsFormOverlayList = $this->viewBuilderFactory
->createFormOverlayListViewBuilder(self::DATA_SET_EDIT_FORM_GROUPS, '/groups')
->setResourceKey(self::DATA_SET_GROUPS)
->setListKey(self::DATA_SET_GROUPS)
->addListAdapters(['table'])
->addRouterAttributesToListRequest(['id' => 'dataSetId'])
->setFormKey(self::DATA_SET_GROUPS)
->addRouterAttributesToFormRequest(['id' => 'dataSetId'])
->setTabTitle('app.data_groups')
->addToolbarActions([
new ToolbarAction('sulu_admin.add'),
new ToolbarAction('sulu_admin.delete')
])
;
$viewCollection->add($groupsFormOverlayList->setParent(self::DATA_SET_EDIT_FORM));
}
}
As of now
I can perform all CRUD operations on DataSet entities.
I can list and remove DataGroup entities belonging to a DataSet.
When trying to add a new DataGroup, I can't perform autocomplete search for nextGroup because the dataSetId parameter is not passed to the field, forbidding the ResourceRequester to perform the REST request.
How do I pass dataSetId to fields of the "New DataGroup" form in order to search for matching entities only?
Thanks a lot for the detailed description! Unfortunately, I am afraid that it is not possible to implement what you are trying to do while using the built-in autocomplete component at the moment.
The resource_store_properties_to_request param reads the values from the data of the form that renders the autocomplete component. If you are creating a new DataGroup entity, the data of the form is empty (because there is now existing data for a new entity) and therefore the resource_store_properties_to_request param is not able to read a dataSetId value.
I am sorry to say, but I think you would need to implement a custom autocomplete field-type that reads the dataSetId value from the current url to achieve the desired functionality. If you are interested in doing this, I would recommend to have a look at the basic field-type example in the sulu-demo repository.

Magento 2 checkout_onepage_controller_success_action event not fired

I am working on Magento 2 platform.
I have created my custom modue name MerchantTrack.
Into events.xml (MagentoSite\app\code\Magento\MerchantTrack\Checkout\etc\frontend\events.xml) written code
below
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="checkout_onepage_controller_success_action ">
<observer name="merchanttrack_checkout_onepage_controller_success_action" instance="Magento\MerchantTrack\Checkout\Observer\MyObserver" />
</event>
</config>
Into MyObserver.php(MagentoSite\app\code\Magento\MerchantTrack\Checkout\Observer\MyObserver.php) written code below
<?php
namespace Magento\MerchantTrack\Checkout\Observer;
use Magento\Framework\Event\ObserverInterface;
class MyObserver implements ObserverInterface {
public function execute(\Magento\Framework\Event\Observer $observer)
{
$orderIds = $observer->getEvent()->getOrderIds();
echo $orderId = $orderIds[0]; exit;
}
}
When placed order, into success (/MagentoSite/checkout/onepage/success/) page i can not see orderid which i echo into observer page. So I can not understand my event is fired or not.
What am I doing wrong?
You have to follow module directory structure properly.
See:
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure
MagentoSite\app\code\Magento\MerchantTrack\Checkout\etc\frontend\events.xml
You have created extra folder i.e Magento
Your directory should look like this.
MagentoSite\app\code\MerchantTrack\Checkout\etc\frontend\events.xml

Block not deleted from DB

Situation:
I'm developing a package for Concrete 5 Version 5.6.3.1.
The problem:
When deleting an already published block from the frontend (like any
user does), the corresponding row in the DB-table isn't deleted.
When deleting the block without publishing, it works.
Here's my controller
class PcShooterChShowSomeThingsBlockController extends Concrete5_Controller_Block_Content {
protected $btName = "Show Some Things";
protected $btTable = 'btPcShooterChShowSomeThings';
protected $btInterfaceWidth = 500;
protected $btInterfaceHeight = 400;
protected $btWrapperClass = 'ccm-ui';
//...
public $pkgHandle = 'pc_shooter_ch_show_some_things';
//...
/**
* --------------- Overrides ----------------------
*/
public function delete() {
$db = Loader::db();
//Log::addEntry('DELETE FROM ' . $this->btTable . ' WHERE bID = ' . $this->bID);
$db->Execute('DELETE FROM ' . $this->btTable . ' WHERE bID = ' . $this->bID);
parent::delete();
}
}
The block itself works well, the package too. It isn't My first package at all, also I develop block/packages as recommended by C5.
I start thinking, its a bug, but before I post something on C5, I'm interested to hear from other C5 developers...
In the forum of C5 it says, that overriding the Concrete5_Controller_Block_Content's delete-method helps.
Also I tried to call the parent::delete(); at the beginnig instead of the end, but no difference.
UPDATE
The parents delete method from the Concrete5_Library_BlockController:
/**
* Automatically run when a block is deleted. This removes the special data
* from the block's specific database table. If a block needs to do more
* than this this method should be overridden.
* #return $void
*/
public function delete() {
if ($this->bID > 0) {
if ($this->btTable) {
$ni = new BlockRecord($this->btTable);
$ni->bID = $this->bID;
$ni->Load('bID=' . $this->bID);
$ni->delete();
}
}
}
UPDATE 1
Cache settings print screen
And, maybe it helps, the db.xml
<?xml version="1.0"?>
<schema version="0.3">
<table name="btPcShooterChShowSomeThings">
<field name="bID" type="I">
<key />
<unsigned />
</field>
<field name="desc_true" type="I2">
</field>
<field name="block_css_style" type="C" size="255">
</field>
<field name="block_css_id" type="C" size="255">
</field>
<field name="block_css_class" type="C" size="255">
</field>
<field name="title_css_class" type="C" size="255">
</field>
<field name="desc_css_class" type="C" size="255">
</field>
</table>
</schema>
If you need any further infos or code, just tell me. I'd be glad to get some tips on this.
UPDATE 2
A uninstall/install of the package does not help neither.
UPDATE 3
When deleting an already published block from the frontend (like any
user does), the corresponding row in the DB-table isn't deleted
and no delete() method is firing, neither myne nor the parent's
So finally, I've got an answer from a developer of the C5-team:
Not a bug. Blocks still store their data so that they can be
reinstated in case a previous version of a page gets approved and
rolled back to. Block's will only call BlockController::delete() when
they no longer need to keep their data around.
For other C5 developers:
Go to Dashboard > System & Settings > Automated Jobs (under "Optimization") and run the job:
"Remove Old Page Versions"
The child's (or parent's) delete method is firing.
The block is deleted.
Thx to Andrew Embler from the C5-Team!

Categories