Drupal 8 - Accessing a referenced entity through $fields (implementing validation on node creation) - validation

Within a custom module, I'm working on validating a field based on an entry from another field. The validation works when the hash is hard coded (i.e. // $bookhash = 1;). However, I cannot seem to figure out how to access hash of the referenced book.
What's the proper way of accessing that data from book referenced within book_signatures?
use \Drupal\Core\Entity\EntityTypeInterface;
/**
* Implements hook_entity_bundle_field_info_alter().
*/
function custom_validation_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
if ($bundle === 'book_signatures') {
if (isset($fields['field_confirm_book_hash'])) {
$book = $fields['field_book']; // book is a referenced entity within book_signatures.
$bookhash = $book->field_hash_check; // need to set this equal to the hash in the book entity.
// $bookhash = 1; works with static hash. need specific books hash
$fields['field_confirm_book_hash']->addConstraint('BookHash', ['hash' => $bookhash]);
}
}
}
Edit: output from devel on the field $fields['field_book']. It's id outputs "node.book_signatures.field_book"

Taking a slightly different approach exposed both fields more elegantly.
/**
* Implements hook_entity_type_build().
*/
function custom_validation_entity_type_build(array &$entity_types) {
// Add our custom validation to the order number.
$entity_types['node']->addConstraint('BookHash');
}

Related

Automatic filter on attribute depending on authenticated user

I have an entity called event, the event can have many rooms and a room can have many participants.
If I access all events (with a specific user) I can filter events where the user has no access right (no room with a connection to the specific user) by using extensions.
That works fine.
The response contains all events which have at least one room with access rights.
But If the event has multiple rooms and the user has only access to one room. The response includes both rooms. I created a RoomExtension, but this class will not be invoked.
Thanks
Your problem is caused by the fact that filters and extensions only work on the query that retrieves the primary entities. The related entities are retrieved using Doctrines associations wich are part of the domain model that is meant to be the single source of truth for all purposes. What you need is a user-specic view on that model, which in the context of api's usually consists of DTOs.
I think there are basically two solutions:
Query primarily for Events and convert the into EventDTOs, then either query for - or filter out - the related Rooms,
Query primarily for Rooms, then group them into EventDTOs.
I explain the second solution here because i guesss that it is simpeler and it shoud make your RoomExtension work out of the box, which makes it the better fit to your question, but also because i happen to have built and tested something similar in a tutorial so it is a lot less work to write an answer with confidence.
The downside of this solution is that it does not support pagination.
Bucause this solution primarily queries for Rooms, the the operation is on the Room resource. If it where the only collectionOperation of Room it could be like this:
(..)
* #ApiResource(
* collectionOperations={
* "get_accessible_events"={
* "method"="GET",
* "path"="/rooms/accessible-events",
* "output"=EventDTO::class,
* "pagination_enabled"=false
* }
* }
* }
*/
class Room {
(..)
(This does not have to be your only collectionOperation, you can still have "get", "post" and others).
Right now this still produces a flat collection of Rooms, you need to group them into EventDTOs. The DTOs page of the docs suggest to make a DataTransformer to produce the DTOs, but that only works if your DTOs are one to one with the entities retrieved by the query. But a CollectionDataProvider can do the trick. Because you do not need to adapt the query itself you can simply decorate the default CollectionDataProvider service:
namespace App\DataProvider;
use ApiPlatform\Core\Api\OperationType;
use App\DTO\EventDTO;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\Room;
class RoomAccessibleEventCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider)
{
$this->dataProvider = $dataProvider;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Room::class === $resourceClass
&& $operationName == 'get_accessible_events';
}
/**
* {#inheritdoc}
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$rooms = $this->dataProvider->getCollection($resourceClass, $operationName, $context);
$dtos = [];
foreach ($rooms as $room) {
$key = $room->getId();
if (isset($dtos[$key])) {
$dtos[$key]->addRoom($room);
} else {
$dto = new EventDTO($room->getEvent());
$dto->addRoom($room);
$dtos[$key] = $dto;
}
}
return $dtos;
}
}
You do need to configure the service in config/services.yaml:
'App\DataProvider\RoomAccessibleEventCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
This does not replace the default CollectionDataProvider but adds another one that gets the default one injected.
I guess you can make the EventDTO class yourself now. Then it should work. Filters defined on Room will also work as usual, for example if rooms can be filtered by the date of their event ?event.date[gte]=2020-10-10 will only find rooms with events on or after 2020-10-10 and return their EventDTO's.
However, in the swagger docs the get_accessible_events operations summary and descriptions still come from Room. You can look up how to add a SwaggerDecorator in the docs or take a look at the chapter9-api branch of the tutorial. The latter also contains complete explanations and tested code for entities, the DTO (Report Model) and an extension for only showing data the user is authorized for, but is not taylored to your questions and would all together be way beyond what a to the point answer.
I can not give you any more hints on this site with respect the other solution because this site will probably see them as an incomplete or unclear answer and punish me for it.

API Platform - Which approach should I use for creating custom operation without entity

I'm new to API Platform. I think it's great but I cannot find any example how to create custom endpoint that isn't based on any entity. There are a lot of examples based on an entity and usually they are all about CRUD. But what about custom operations?
I need to create custom search through database with some custom parameters which aren't related to any entity.
E.g. I want to receive POST request something like this:
{
"from": "Paris",
"to": "Berlin"
}
This data isn't saved to db and I haven't entity for it.
After I receive this data, there should be a lot of business logic including db queries through a lot of db tables and also getting data from external sources.
Then, after the business logic is finished, I want to return back result which is also custom and isn't related to any entity.
E.g.
{
"flights": [/* a lot of json data*/],
"airports": [/* a lot of json data*/],
"cities": [/* a lot of json data*/],
.......
}
So, I think I'm not the only on who does something similar. But I really cannot find a solution or best practices how to do this.
In the documentation I've found at least three approaches and I cannot implement none of them.
The best one, I guess the most suitable for me it is using Custom Operations and Controllers. But documentation says this one is not recommended. Also I think I should use DTOs for request and response, but for this approach I'm not sure I can use them.
The second one I found it's using Data Transfer Objects, but this approach requires an entity. According to the documentation, I should use DTOs and DataTransformers to convert DTO to an Entity. But I don't need entity, I don't need save it to db. I want just handle received DTO on my own.
The third one I guess it is using Data Providers, but I'm not sure it is suitable for my requirements.
So, the main question is which approach or best practice should I use to implement custom operation which isn't related to any entity. And it will be great use DTOs for request and response.
You are not forced to use entities. Classes that are marked with #ApiResource annotation may not be entities. Actually, if your application is smarter than basic CRUD you should avoid marking entities as ApiResource.
Since you want to use POST HTTP method (which is for creating resource items) you can do something like this.
1) Define class describing search fields and which will be your #ApiResource
<?php
// src/ApiResource/Search.php
namespace App\ApiResource;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Action\NotFoundAction;
use ApiPlatform\Core\Annotation\ApiProperty;
use App\Dto\SearchResult;
/**
* #ApiResource(
* itemOperations={
* "get"={
* "controller"=NotFoundAction::class,
* "read"=true,
* "output"=false,
* },
* },
* output=SearchResult::class
* )
*/
class Search
{
/**
* #var string
* #ApiProperty(identifier=true)
*/
public $from;
/** #var string */
public $to;
}
2) Define DTO that will represent the output
<?php
// src/Dto/SearchResult.php
namespace App\Dto;
class SearchResult
{
public $flights;
public $airports;
public $cities;
}
3) Create class that will inplement DataPersisterInterface for handling business logic.
It will be called by framework because you make POST request.
<?php
// src/DataPersister/SearchService.php
declare(strict_types=1);
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\SearchResult;
use App\ApiResource\Search;
final class SearchService implements DataPersisterInterface
{
public function supports($data): bool
{
return $data instanceof Search;
}
public function persist($data)
{
// here you have access to your request via $data
$output = new SearchResult();
$output->flights = ['a lot of json data'];
$output->airports = ['a lot of json data'];
$output->cities = ['inputData' => $data];
return $output;
}
public function remove($data)
{
// this method just need to be presented
}
}
That way you will recieve results based on request.

Yii2: How to set default attribute values in ActiveRecord?

This may seem like a trivial question, however all of the obvious solutions that I can think of have their own flaws.
What we want is to be able to set any default ActiveRecord attribute value for new records only, in a way that makes it readable before and during validation and does not interfere with derived classes used for search.
The default values need to be set and ready as soon as we instantiate the class, so that (new MyModel)->attr returns the default attr value.
Here are some of the possibilities and the problems they have:
A) In MyModel override the init() method and assign default value when isNewRecord is true like so:
public function init() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
parent::init();
}
Problem: Search. Unless we explicitly unset our default attribute in MySearchModel (very error-prone because it is too easy to forget), this will also set the value before calling search() in the derived MySearchModel class and interfere with searching (the attr attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by calling unsetAttributes() before calling search(), however no such method exists in Yii2.
B) In MyModel override the beforeSave() method like so:
public function beforeSave($insert) {
if ($insert) {
$this->attr = 'defaultValue';
}
return parent::beforeSave();
}
Problem: Attribute is not set in unsaved records. (new MyModel)->attr is null. Worse yet, even other validation rules that rely on this value will not be able to access it, because beforeSave() is called after validation.
C) To ensure the value is available during validation we can instead override the beforeValidate() method and set the default values there like so:
public function beforeValidate() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
return parent::beforeValidate();
}
Problem: Attribute is still not set in unsaved (unvalidated) records. We need to at least call $model->validate() if we want to get the default value.
D) Use DefaultValidator in rules() to set a default attribute value during validation like so:
public function rules() {
return [
[
'attr', 'default',
'value' => 'defaultValue',
'on' => 'insert', // instantiate model with this scenario
],
// ...
];
}
Problem: Same as B) and C). Value is not set until we actually save or validate the record.
So what is the right way to set default attribute values? Is there any other way without the outlined problems?
There's two ways to do this.
$model => new Model();
Now $model has all the default attributes from the database table.
Or in your rules you can use:
[['field_name'], 'default', 'value'=> $defaultValue],
Now $model will always be created with the default values you specified.
You can see a full list of core validators here http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html
This is a hangup with Yii's bloated multi-purpose ActiveRecords
In my humble opinion the form models, active records, and search models would be better off split into separate classes/subclasses
Why not split your search models and form models?
abstract class Creature extends ActiveRecord {
...
}
class CreatureForm extends Creature {
public function init() {
parent::init();
if ($this->isNewRecord) {
$this->number_of_legs = 4;
}
}
}
class CreatureSearch extends Creature {
public function search() {
...
}
}
The benefits of this approach are
You can easily cater for different validation, set up and display cases without resorting to a bunch of ifs and switches
You can still keep common code in the parent class to avoid repetition
You can make changes to each subclass without worrying about how it will affect the other
The individual classes don't need to know about the existence of any of their siblings/children to function correctly
In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all
I know it is answered but I will add my approach.
I have Application and ApplicationSearch models. In Application model I add init with a check of the current instance. If its ApplicationSearch I skip initializations.
public function init()
{
if(!$this instanceof ApplicationSearch)
{
$this->id = hash('sha256', 123);
}
parent::init();
}
also as #mae commented below you can check for existence of search method in current instance, assuming you didn't add any method with name search to the non-search base model so the code becomes:
public function init()
{
// no search method is available in Gii generated Non search class
if(!method_exists($this,'search'))
{
$this->id = hash('sha256', 123);
}
parent::init();
}
I've read your question several times and I think there are some contradictions.
You want the defaults to be readable before and during validation and then you try init() or beforeSave(). So, assuming you just want to set the default values in the model so they can be present during the part of the life cycle as long as possible and not interfere with the derived classes, simply set them after initialising the object.
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
$model->setDefaultValues();
Or you can create static method to create model with all default values set and return the instance of it.
$model = Model::createNew();
Or you can pass default values to constructor.
$model = new Model([
'attribute1' => 'value1',
'attribute2' => 'value2',
]);
This is not much different from setting the attributes directly.
$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';
Everything depends on how much transparent would you like your model be to your controller.
This way attributes are set for the whole life cycle except the direct initialisation and it's not interfering with derived search model.
Just override __construct() method in your model like this:
class MyModel extends \yii\db\ActiveRecord {
function __construct(array $config = [])
{
parent::__construct($config);
$this->attr = 'defaultValue';
}
...
}
If you want to load default value from database you can put this code in your model
public function init()
{
parent::init();
if(!method_exists($this,'search')) //for checking this code is on model search or not
{
$this->loadDefaultValues();
}
}
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
if($model->isNewRecord())
$model->setDefaultValues();

Retrieving records from database using eloquent with optional query parameters

i have the following block of code in my Resource Controller:
$travel_company_id = Input::get('travel_company_id');
$transport_type = Input::get('transport_type');
$route_type = Input::get('route_type');
$travelRoutes = TravelRoute::where('travel_company_id', $travel_company_id)
->where('transport_type', $transport_type)
->where('route_type', $route_type)
->get();
Now what this does is it gets travelRoutes based on the parameters supplied. What i want is for it to do is perform a search based on the available parameters, that way if $route_type is empty the search will be performed only on travel_company_id and transport type.
Also if all the parameters are empty then it will simply do a get and return all available records.
I know i can do this with lots of if statements but then if i add a new parameter on the frontend i will have to add it to the backend as well, I was wondering if there was a much simpler and shorter way to do this in laravel.
The where method accepts an array of constraints:
$constraints = array_only(Input::all(), [
'travel_company_id',
'transport_type',
'route_type',
]);
$routes = TravelRoute::where($constraints)->get();
Warning: do not use Input::only() instead of array_only(). They're not the same.
Input::only() fills in any missing items with null, which is not what you want here.
This is pretty hacky and if you spend some time developing a solution I'm sure it could be much nicer. This assumes all the fields in the getSearchFields() function match the input names from the form and database.
/**
* Search fields to retrieve and search the database with. Assumed they match the
* column names in the database
*/
private function getSearchFields()
{
return ['travel_company_id', 'transport_type', 'route_type'];
}
public function search()
{
// Get a new query instance from the model
$query = TravelRoute::query();
// Loop through the fields checking if they've been input, if they have add
// them to the query.
foreach($this->getSearchFields() as $field)
{
if (Input::has($field))
{
$query->where($field, Input::get($field));
}
}
// Finally execute the query
$travelRoutes = $query->get();
}

Yii2: Eagerly selecting calculated column and loading value into model-property

I thought I know every aspect of Yii2 in the meantime, but this one gives me headaches.
Situation
Two tables: Client and Billings. The Client-Table holds a regular list of clients. The Billing-table has several entries for each client (1:n).
Problem
I want to fetch a calculated DB-Field together with the row itself and access it via a virtual property of the model.
Key is that it gets calculated and selected together with the row itself. I know I can achieve something similliar with a regular virtual getter calculating the amount...but this is not at the same time as the select itself.
My Plan
In the query-object of the client-model i tried to add an an additional select (addSelect-Method) and give the field an alias. Then I added the alias of this select with the attributes-method of the model. Somehow this didn't work.
My Question
Does someone of you know the right way to achieve this? As this is a very common problem, I can not imagine this beeing too hard. I just somehow can't find the solution.
Sample code:
echo $client->sumOfBillings should output the contents of the corresponding property within the client-model. The contents of this property should be filled when fetching the client-row itself and not at the moment the property gets called.
I actual found the answer myself. Here is how you do it:
Query object
The fetching of all the Yii2-Models is done via their corresponding Query-Object. This object is retrieved via the models find()-Method. If you override this method, you can return your own query-object for that class. In the example above my model looks like this:
class Client extends \yii\db\ActiveRecord
{
//...
public static function find()
{
return new ClientQuery(get_called_class());
}
//...
}
Now within the Query-Objects init()-Method we can add the corresponding additional selects:
public class ClientQuery extends \yii\db\ActiveQuery
{
public function init()
{
parent::init();
//prepare subquery for calculation
$sub = (new Query())
->select('SUM(billing_amount)')
->from('billing')
->where('billing.client_id = client.id');
$this->addSelect(['client.*', 'sumBillings'=>$sub]);
}
}
We are now done with the query-Object. What have we done now? When selecting a client the sum gets calculated and loaded as well. But how do we access it? This was the hard part where I struggeled. The solution lies within the ActiveRecord-class.
Possibilities to populate the model with calculated data
There are several possibilities to load this data into the model-class. To understand what options we have, we can check out the populateRecord($record, $row)-method of the BaseActiveRecord-class:
/**
* Populates an active record object using a row of data from the database/storage.
*
* This is an internal method meant to be called to create active record objects after
* fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
* the query results into active records.
*
* When calling this method manually you should call [[afterFind()]] on the created
* record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
*
* #param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
* created by [[instantiate()]] beforehand.
* #param array $row attribute values (name => value)
*/
public static function populateRecord($record, $row)
{
$columns = array_flip($record->attributes());
foreach ($row as $name => $value) {
if (isset($columns[$name])) {
$record->_attributes[$name] = $value;
} elseif ($record->canSetProperty($name)) {
$record->$name = $value;
}
}
$record->_oldAttributes = $record->_attributes;
}
As you can see, the method takes the raw-data ($row) and populates the model instance ($record). If the model has either a property or a setter-method with the same name as the calculated field, it will be populated with data.
Final code of Client-Model
This is my final code of the Client-model:
class Client extends \yii\db\ActiveRecord
{
private $_sumBillings;
//...
public static function find()
{
return new ClientQuery(get_called_class());
}
public function getSumBillings()
{
return $this->_sumBillings;
}
protected function setSumBillings($val)
{
$this->_sumBillings = $val;
}
//...
}
The populateRecord()-method will find the setter-method ($record->canSetProperty($name)) and call it to fill in the calculated value. As it is protected, it is otherwise readonly.
VoilĂ ...not that hard actually and definitely useful!

Resources