Symfony2, Doctrine2: non english/letin slug - utf-8

i have a community, where users can make their own topics, also in international languages like hebrew or arabic
i have the "name" field of the topic, and i want to generate slugs out of it
now i have tested it with arabic, using the stofdoctrineBundle and Doctrine extensions
i set up the slug like this
/**
* #Gedmo\Slug(fields={"name"})
* #ORM\Column(length=255, unique=true, nullable=false)
*/
private $slug;
and when i try to make a new topic with arabic, say this string "علي قرا", doctrine sluggable generated the following slug: ly-qr
so my goal is to have complete URL like this
http://www.mysite.com/علي-قرا
for SEO reasons.

Florian, i tried your way, but it looked way too complicated for my needs, so i went an easier path:
completely disabled doctrine extensions
created a new class under Utils\MyFunctions, and put the static method slugify which is based on symfony2 blog tutorial.
when setting the name, i also set the slug with it
public function setName($name)
{
$this->name = $name;
$this->setSlug($this->name);
}
public function setSlug($slug)
{
//now we generate the slug, from the utilities my function
$slug = Myfunctions::slugify($slug);
$this->slug = $slug;
}
Problem solved, and arabic now showing fine as a slug.

The default behavior of the slugifier in gedmo's doctrine extensions is to convert all characters to ASCII.
If you don't want this behavior, you will have to create either your own handler and/or your own transliterator method.
The docs here explain well how to do it.

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.

Relation name converting from camelCase to snake_case

I have a old project on Illuminate Database 4.2 and now i trying to upgrade it to 6.0, but i found some weird stuff:
i have relation like:
public function addressTV()
{
return $this->hasMany('Billing\db\Models\AddrMapTv', 'StreetID');
}
in 4.2 when i use
$list = \Billing\db\Models\Street::with(['addressTV'])->get()->toArray();
it transforming to:
$list['address_tv']
Its ok.
But in 6.0 with same code i got:
$list['address_t_v'];
This creates a lot of problems for me. Is there any way to return the old name conversion?
It might get a bit difficult if you have many relations defined this way, however, the easiest way I see here is to define accessor
Your issue here is that address_tv key is absent, defining an accessor will add that key.
So in your Street model:
/**
* Get the street's address_tv.
*
* #return object
*/
public function getAddressTvAttribute()
{
return $this->addressTV();
}
did not test it but it should work in theory.

Get hidden attribute of a model specified using Model::with()

In a controller, I use this code to get all Text objects and their associated authors
return Text::with('authors')->get();
This is part of a backend only available to admins, and I need them to be able to access the authors name fields. But in the Author model, I set protected $hidden = ['name']; when I programmed another part of my app that's for standard users.
There's a hasMany relationship: Each text has many authors. Is there a way to use with, but get some hidden attribute? Or the other way round, to declare some attribute as hidden temporarily when using with?
Please note this is question is about the use of with in combination with hidden attributes. I'm not looking for something like $authors = $authors->makeVisible(['name']); (which is explained here).
I could find two approaches to solve my problem:
1) Using each as seen in this question
$texts = Text::with('authors')
->get()
->each(function ($text) {
$text->authors->makeVisible(['name']);
});
2) Using transform() as recommended in an answer to the same question. This seems to be way faster.
$texts = Text::with('authors')
->get()
->transform(function ($text) {
$text->authors->makeVisible(['name']);
return $text;
});
Try this code:
Text::with(['author'=>function($q){
$q->makeVisible('name')
}])->get();

Cakephp: generic validation rules in AppModel?

I'm wondering what's the "best" approach to validate fields generically. In my application several tables have date values that are always entered using a date picker widget. I don't want to repeat the validation code, so I would like to do something like filling the $validate array in the AppModel. But it gets overwritten in the concrete model class. The best I found so far is the paragraph "Dynamically change validation rules" in the cake book, and apply that logic to the AppModel somehow, but it looks a bit hacky and un-caky. Does anyone have a hint?
(If you have questions, please ask.)
Thanks
Just name them differently - unique so to speak:
public function validateDateTime() {}
etc. This way your custom rules don't verwrite the core rules and vica versa.
I had some validation rules that I wanted to put in 3 models, to not repeat the same code, here what I did
in AppModel.php, define some var with those rules that should be in multiple models.
public $validationRules = arra(
// rules here
);
and add them for necessary models in AppModel's constructor
public function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
/**
* add validation
*/
if (in_array($this->alias, array('MyModel1', 'MyModel2', 'MyModel3')) ) {
$this->validate = array_merge($this->validate, $this->validationRules);
}
}
if there are some custom validation functions, those can be moved to AppModel.php as well.

How to count many-to-many relations in Symfony2 using DQL

I would like to count the number of tags given a specific article. I've got two entities (Article, Tag) which are related by a many-to-many association:
//Bundle/Entity/Article.php
/**
* #ORM\ManyToMany(targetEntity="Tag")
*/
private $tags;
Now I've got n articles with m tags and I would like to know how often a specific tag has been used.
I'm relatively new to both Symfony2 and Doctrine. The problem is that I don't know where to fit such a query (I guess it should reside in the ArticleRepository but on the other hand it would make sense to have it in the TagRepository) and how to JOIN the correct tables (in this case Article, article_tag, Tag).
The simplest way that I can think of is to just set up a bidirectional relationship between Article and Tag:
class Article
{
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="articles")
*/
private $tags;
}
class Tag
{
/**
* #ORM\ManyToMany(targetEntity="Article", mappedBy="tags")
*/
private $articles;
}
Then you can (assuming you've set up standard getters and setters) use $tag->getArticles()->count();, where $tag is a managed Tag entity, to get the number of articles attached to that tag. This works because when populating ToMany relationship properties, Doctrine uses an instance of Doctrine\Common\Collections\ArrayCollection. Check out the source here.
Also, if you go this route, make sure you read the documentation on picking an owning and inverse side here.
You can count the number of tags by using a specific article..lets say article id=5:
$query=$em->createQuery("SELECT count(t.id) FROM Tag t WHERE ?1 MEMBER OF t.articles");
$query->setParameter(1,5 );
$result = $query->getSingleScalarResult();
This gives me the number of tags in Article Entity only for article id = 5.

Resources