API Platform & Symfony: what is the correct way to make a custom controller for collection operations? - api

As far as I understand, Api Platform deletes and updates one by one the resources of a Collection. For example, I have an Activity Entity, which has a Many-To-One relation with OpeningHours: if I want to delete or add multiples OpeningHours through an Admin Back Office, I must call a "delete" operation for each openingHour with its unique #id. As long as I have very few OpeningHours, it's quite all right : the job is done in a few seconds. But what am I supposed to do when I have thousand of them ? Just wait ?
So I've created a Custom Controller and a Custom Route - so far only for the Delete Operation ; the update operation will come after.
First Question : is this the right way to do it or have I missed something in the docs?
Here is my API configuration for the Activity Entity:
/**
* #ApiResource(
* collectionOperations={
* "get"={"normalization_context"={"groups"="activity:list"}},
* "post"={"denormalization_context"={"groups"="activity:post"}},
* },
* itemOperations={
* "get"={"normalization_context"={"groups"="activity:item"}},
* "put"={"denormalization_context"={"groups"="activity:item"}},
"delete_opening_hours"={
* "method"="DELETE",
* "path"="/admin/activity/{id}/delete_opening_hours",
* "controller"=DeleteOpeningHoursAction::class,
* "read"=false,
* },
* "delete", "patch"
* },
* attributes={"pagination_items_per_page"=10}
* )
And my Custom Controller :
/**
* Class DeleteOpeningHoursAction
* #Security("is_granted('ROLE_ADMIN')")
*/
final class DeleteOpeningHoursAction extends AbstractController
{
/**
* #Route(
* name="delete_opening_hours",
* path="/admin/activity/{id}/delete_opening_hours",
* methods={"DELETE"},
* )
*/
public function deleteHours(Activity $data):Activity
{
$em = $this->getDoctrine()->getManager();
$activity = $em->getRepository('App\Entity\Activity')->find($data);
$openingHours = $activity->getOpeningHours();
foreach ($openingHours as $hour) {
$em->remove($hour);
}
$em->flush();
$response = new Response();
$response->setStatusCode(204);
return $response;
}
}
It does the requested job: all the OpeningHours are deleted at once, but it returns a 500 Error:
Return value of App\Controller\Action\DeleteOpeningHoursAction::deleteHours() must be an instance of App\Entity\Activity, instance of Symfony\Component\HttpFoundation\Response returned
And if I return $activity instead of the response above, the error message becomes :
Return value of App\Controller\Action\DeleteOpeningHoursAction::deleteHours() must be an instance of Symfony\Component\HttpFoundation\Response, instance of App\Entity\Activity returned
So what am I doing wrong? What is the correct response for a Custom Controller?
I'm quite confused.
Thanks for any help.

Related

error when seeding data into a database in laravel 8

am getting this error when seeding data into my database. apparently the I have 2 models bookings and bookingstatus model.the bookingstatus seeder is working well and is seeding the status very well but when it comes to the bookings the data is unable to seed.this is the error that pops up
ErrorException
Undefined variable:
at F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php:73
69▕ 'event_details'=>'we would like to book you for a wedding'
70▕
71▕ ]);
72▕ $Approved->bookingstatus()->attach($Approvedstatus);
➜ 73▕ $Cancelled->bookingstatus()->attach($$Cancelledstatus);
74▕ $DepositPaid->bookingstatus()->attach($DepositPaidstatus);
75▕ $Published->bookingstatus()->attach($Publishedstatus);
76▕ }
77▕ }
1 F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php:73
Illuminate\Foundation\Bootstrap\HandleExceptions::handleError("Undefined variable: ", "F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php")
2 F:\Main Server\htdocs\voskillproject\vendor\laravel\framework\src\Illuminate\Container\BoundMethod.php:36
Database\Seeders\BookingsSeeder::run()
this is my BookingsFactory
<?php
namespace Database\Factories;
use App\Models\Bookings;
use Illuminate\Database\Eloquent\Factories\Factory;
class BookingsFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Bookings::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'is_booking'=>0,
'full_name'=>$this->faker->unique()->name,
'location'=>$this->faker->location,
'phone'=>$this->faker->number,
'is_booking'=>3,
'email'=>$this->faker->safeEmail,
'date'=>$this->faker->date,
'event_id'=>$this->faker->event_id,
'event_details'=>$this->faker->paragraph,
];
}
}
this is my database seeder
<?php
namespace Database\Seeders;
use App\Models\Role;
use App\Models\Bookingstatus;
use Illuminate\Database\Seeder;
use Database\Seeders\RoleSeeder;
use Database\Seeders\Userseeder;
use Database\Seeders\BookingsSeeder;
use Database\Seeders\BookingstatusSeeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* #return void
*/
public function run()
{
$this->call([
BookingsSeeder::class,
BookingstatusSeeder::class,
]);
\App\Models\Bookingstatus::factory()->hasbookings(10)->create();
}
}
i have not understood where ie gone wrong in the code
i figure out where the issue was
1.first on the bookings factory i have to change the faker details to this
return [
'is_booking'=>0,
'full_name'=>$this->faker->unique()->name,
'location'=>$this->faker->address,
'phone'=>$this->faker->numerify('###-###-####'),
'is_booking'=>3,
'email'=>$this->faker->safeEmail,
'date'=>$this->faker->unique()->dateTime()->format('Y/m/d'),
'event_id'=>$this->faker->randomDigit(),
'event_details'=>$this->faker->paragraphs(2, true),
];
also i should have imported this code at the top of the bookingstatus factory
use Illuminate\Support\Str;

Symfony 4 serialize entity without relations

I have to log the changes of each entity. I've Listener which listens for doctrine's events on preRemove, postUpdate and postDelete.
My entity AccessModule has relations:
App\Entity\AccessModule.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\AccessModule", mappedBy="parent")
* #ORM\OrderBy({"id" = "ASC"})
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\AccessModule", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\AccessModuleRoute", inversedBy="access_modules")
* #ORM\JoinTable(name="access_routes",
* joinColumns={#ORM\JoinColumn(name="access_module_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="route_id", referencedColumnName="id")})
*
*/
private $routes;
in listener:
App\EventListener\EntityListener.php
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$this->serializer = new Serializer([$normalizer], $encoders);
public function createLog(LifecycleEventArgs $args, $action){
$em = $args->getEntityManager();
$entity = $args->getEntity();
if ($this->tokenStorage->getToken()->getUser()) {
$username = $this->tokenStorage->getToken()->getUser()->getUsername();
} else {
$username = 'anon'; // TODO Remove anon. set null value
}
$log = new Log();
// $log->setData('dddd')
$log->setData($this->serializer->serialize($entity, ''json)
->setAction($action)
->setActionTime(new \DateTime())
->setUser($username)
->setEntityClass(get_class($entity));
$em->persist($log);
$em->flush();
}
I've problem with serialization
When I use $log->setData($entity) I get problem with Circular.
Whan I do serialization $log->setData($this->serializer->serialize($entity, ''json) I get full of relations, with parent's children, with children children. In a result I get full tree :/
I'd like to get
Expect
[
'id' => ID,
'name' => NAME,
'parent' => parent_id // ManyToOne, I'd like get its id
'children' => [$child_id, $child_id, $child_id] // array of $id of children array collection
]
(ofcourse this is draft before encode it to json)
How can I get expected data without full relations?
Thing you are looking for is called serialization groups: here and here.
Now let me explain how it works. It's quite simple. Say you have Post Entity:
class Post
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User\User")
* #Groups({"default"})
*/
private $author;
}
And you have also User Entity:
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\Column(type="string", length=40)
* #Groups({"default"})
*/
private $firstName;
/**
* #ORM\Column(type="string", length=40)
*/
private $lastName;
}
Post can have author(User) but I don't want to return all User data every single time. I'm interested only in id and first name.
Take a closer look at #Groups annotation. You can specify so called serialization groups. It's nothing more than convinient way of telling Symfony which data you would like to have in your result set.
You have to tell Symfony serializer which relationships you would like to keep by adding relevant groups in form of annotation above property/getter. You also have to specify which properties or getters of your relationships you would like to keep.
Now how to let Symfony know about that stuff?
When you prepare/configure your serializaition service you just simply have to provide defined groups like that:
return $this->serializer->serialize($data, 'json', ['groups' => ['default']]);
It's good to build some kind of wrapper service around native symfony serializer so you can simplify the whole process and make it more reusable.
Also make sure that serializer is correctly configured - otherwise it will not take these group into account.
That is also one way(among other ways) of "handling" circular references.
Now you just need to work on how you will format your result set.
ignored_attributes provides a quick, and easy way to accomplish what you're looking for.
$serializer->serialize($object, 'json', ['ignored_attributes' => ['ignored_property']]);
Heres how its used with the serializer:
use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$person = new Person();
$person->setName('foo');
$person->setAge(99);
$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();
$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($person, 'json', ['ignored_attributes' => ['age']]);
Documentation: https://symfony.com/doc/current/components/serializer.html#ignoring-attributes
Tested in Symfony 4.1, here is the documentation that actually works https://symfony.com/blog/new-in-symfony-2-7-serialization-groups
Robert's explanation https://stackoverflow.com/a/48756847/579646 is missing the $classMetadataFactory in order to work. Here is my code:
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$encoders = [new JsonEncoder()];
$normalizer = new ObjectNormalizer($classMetadataFactory);
$normalizer->setCircularReferenceLimit(2);
// Add Circular reference handler
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$normalizers = [$normalizer];
$serializer = new Serializer($normalizers, $encoders);
$jsonContent = $serializer->serialize($jobs, 'json', array('groups' => ['default']));
return JsonResponse::fromJsonString($jsonContent);

Extending news Model does not work for FE and news_ttnewsimport

In order to upgrade an existing system, I have to import extended tt_news records to tx_news. The problem is, that the extending of the tx_news Model seems not to work proper and of course this, the import neither.
But in Backend I can see and store data in my additional fields.
What I've done so far:
I've extended tx_news Version 3.2.8
My Model:
class News extends \GeorgRinger\News\Domain\Model\News {
/**
* uidForeign.
*
* #var int
*/
protected $uidForeign;
/**
* Sets the uidForeign.
*
* #param int $uidForeign
*
* #return void
*/
public function setUidForeign($uidForeign)
{
$this->uidForeign = $uidForeign;
}
/**
* Returns the uidForeign.
*
* #return int $uidForeign
*/
public function getUidForeign()
{
return $this->uidForeign;
}
/**
* tableForeign.
*
* #var string
*/
protected $tableForeign;
/**
* Sets the tableForeign.
*
* #param string $tableForeign
*
* #return void
*/
public function setTableForeign($tableForeign)
{
$this->tableForeign = $tableForeign;
}
/**
* Returns the tableForeign.
*
* #return string $tableForeign
*/
public function getTableForeign()
{
return $this->tableForeign;
}
}
ext_localconf:
$GLOBALS['TYPO3_CONF_VARS']['EXT']['news']['classes']['Domain/Model/News'][] = 'news_extend';
I think that should work. The generated class in typo3temp seems correct. My fields including their getter/setter are in there.
But in Controller and FE I can not access these fields.
What am I missing? What else can I check?
If you upgrade a project, I don't know really the reason why you are using an old version of EXT:news.
What could be missing is the TCA definition of the field.
If you want to migrate from tt_news to news, there is a ready-to-use solution which can be found here https://github.com/ext-news/news_ttnewsimport
The reason was an configuration setting for the backend cache.
they have bees set to TYPO3\CMS\Core\Cache\Backend\NullBackend:class instead TYPO3\CMS\Core\Cache\Backend\NullBackend.
Now it works.

VSCode: How to document promise that resolves with complex object?

I have a function f that returns a Promise. The returned Promise either resolve({name: String, data: Object}) or reject(Error).
I've tried the following syntax(as mentioned in an issue in JSDoc) in VSCode, but it doesn't work:
/**
* #promise fPromise
* #reject {Error}
* #fulfill {Object} project
* #fulfill {Object} project.data
* #fulfill {String} project.name
* #returns fPromise
*/
I think your best bet is to wrap your fulfill response into a custom object:
/**
* #promise fPromise
* #reject {Error}
* #fulfill {Project}
* #returns {Promise.<Project>}
*/
function renderResults(data) {
return new Promise((resolve, reject) => {
resolve(new Project())
})
}
renderResults()
function Project() {
this.data = "data";
this.name = "project phoenix"
this.location = {
city: 'seattle',
state: 'wa'
}
}
This will show in VS Code like this:
To be as clear as possible, why not put the nature of the object on a single line for the description? Its just supposed to be a description of that fulfill.
/**
* #promise fPromise
* #fulfill {Object} A project object with the format {name: String, data: Object}
* #reject {Error}
* #returns fPromise
*/
Or, if you want to handle dynamically generated object keys, similar to the Google Style Guide:
/**
* #promise fPromise
* #fulfill {Object.<String, Object>}
* #reject {Error}
* #returns fPromise
*/
That allows anyone reading your comment to understand what the returned object looks like, what the keys are, and what type of value should be in each key.
Unless, you're trying to say that it can return any of the three possibilities. Then I think your original format is a little bit more descriptive of the possible results of the Promise being fulfilled.

custom Yii validation class

I've created this custom class to validate some numbers on my website.
class EPriceValidator extends CValidator
{
public $number_type;
/*
* Regular Expressions for numbers
*/
private $default_pattern = '/[^0-9,.]/';
private $price_pattern = '/[^0-9,.]/';
/*
* Default error messages
*/
private $default_msg = '{attribute} is an invalid number.';
private $price_msg = '{attribute} is an invalid price.';
/**
* Validates the attribute of the object.
* If there is any error, the error message is added to the object.
* #param CModel $object the object being validated
* #param string $attribute the attribute being validated
*/
protected function validateAttribute($object,$attribute)
{
// check the strength parameter used in the validation rule of our model
if ($this->number_type == 'price')
{
$pattern = $this->price_pattern;
$error_message = $this->price_msg;
}
else {
$pattern = $this->default_pattern;
$error_message = $this->default_msg;
}
// extract the attribute value from it's model object
$value=$object->$attribute;
if(!preg_match($pattern, $value))
{
$this->addError($object,$attribute, $error_message);
}
}
/**
* Implementing Client Validation
*
* Returns the JavaScript needed for performing client-side validation.
* #param CModel $object the data object being validated
* #param string $attribute the name of the attribute to be validated.
* #return string the client-side validation script.
* #see CActiveForm::enableClientValidation
*/
public function clientValidateAttribute($object,$attribute)
{
// check the strength parameter used in the validation rule of our model
if ($this->number_type == 'price')
{
$pattern = $this->price_pattern;
$error_message = $this->price_msg;
}
else
{
$pattern = $this->default_pattern;
$error_message = $this->default_msg;
}
$condition="value.match(".$pattern.")";
return "
if(".$condition.") {
messages.push(".CJSON::encode($error_message).");
}
";
}
}
it works fine. but how do i make it display the correct field name of the error? right now when there is an error detected on client side, the clientValidateAttribute() displays
{attribute} is an invalid number.
instead of
Total orders is an invalid number.
where Total orders is the input field that is in valid.
Any idea how to fix this?
I rechecked this in the Yii documentation, and it seems you have to add an array with parameters to replace the placeholders in your string. But if you only use the default placeholder for the attribute, it should work by default.
Do you have only the problem on client validation? Because I now checked also the Yii code, and it seems that your code is right, and should work (at least the server validation). But in the client validation you just pass the error mesasage to JSON without any processing, so the {attribute} is not replaces anywhere.
Try to add this to youc client validation before the return
$params['{attribute}']=$object->getAttributeLabel($attribute);
$error_message = strtr($error_message,$params));