I have a question - is here a possibility to configure AssociationField to work with specific property. i.e:
I have a Subscription Entity with a many-to-one relation to User, User has a __toString() method, that returns username, and it is used across the application, so I can't change it. In the 'create Subscription' form, I have AssociationField::new('user'), where I'm able to find User by his name.
But this is inconvenient, since, when I need to create a Subscription, many users with same names pop up. Instead, I want to be able to search Users by ID, or email.
Is there a way to override default behaviour?
Your AssociationField is made using Symfony EntityType. If you look into the form type used by this field.
//AssociationField.php
public static function new(string $propertyName, $label = null): self
{
return (new self())
//...
->setFormType(EntityType::class)
//...
It means that you can use every options from it. See more here.
In your case, it's quite easy modifying your label by either defining another property or a callback.
Then you can use the ->setFormTypeOption() to modify the entity type option.
So if you want to use a callback function to define a custom label:
AssociationField::new('user')
->setFormTypeOption('choice_label', function ($user) {
return $user->getEmail();
});
Or using php 7.4 arrow function:
AssociationField::new('user')
->setFormTypeOption('choice_label', fn($user) => $user->getEmail());
You can also define the email property as label:
AssociationField::new('user')
->setFormTypeOption('choice_label', 'email');
Related
I'm trying to use Symfony Voters and Controller Annotation to allow or restrict access to certain actions in my Symfony 4 Application.
As an example, My front-end provides the ability to delete a "Post", but only if the user has the "DELETE_POST" attribute set for that post.
The front end sends an HTTP "DELETE" action to my symfony endpoint, passing the id of the post in the URL (i.e. /api/post/delete/19).
I'm trying to use the #IsGranted Annotation, as described here.
Here's my symfony endpoint:
/**
* #Route("/delete/{id}")
* #Method("DELETE")
* #IsGranted("DELETE_POST", subject="post")
*/
public function deletePost($post) {
... some logic to delete post
return new Response("Deleting " . $post->getId());
}
Here's my Voter:
class PostVoter extends Voter {
private $attributes = array(
"VIEW_POST", "EDIT_POST", "DELETE_POST", "CREATE_POST"
);
protected function supports($attribute, $subject) {
return in_array($attribute, $this->attributes, true) && $subject instanceof Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
... logic to figure out if user has permissions.
return $check;
}
}
The problem I'm having is that my front end is simply sending the resource ID to my endpoint. Symfony is then resolving the #IsGranted Annotation by calling the Voters and passing in the attribute "DELETE_POST" and the post id.
The problem is, $post is just a post id, not an actual Post object. So when the Voter gets to $subject instanceof Post it returns false.
I've tried injecting Post into my controller method by changing the method signature to public function deletePost(Post $post). Of course this does not work, because javascript is sending an id in the URL, not a Post object.
(BTW: I know this type of injection should work with Doctrine, but I am not using Doctrine).
My question is how do I get #IsGranted to understand that "post" should be a post object? Is there a way to tell it to look up Post from the id passed in and evaluated based on that? Or even defer to another controller method to determine what subject="post" should represent?
Thanks.
UPDATE
Thanks to #NicolasB, I've added a ParamConverter:
class PostConverter implements ParamConverterInterface {
private $dao;
public function __construct(MySqlPostDAO $dao) {
$this->dao = $dao;
}
public function apply(Request $request, ParamConverter $configuration) {
$name = $configuration->getName();
$object = $this->dao->getById($request->get("id"));
if (!$object) {
throw new NotFoundHttpException("Post not found!");
}
$request->attributes->set($name, $object);
return true;
}
public function supports(ParamConverter $configuration) {
if ($configuration->getClass() === "App\\Model\\Objects\\Post") {
return true;
}
return false;
}
}
This appears to be working as expected. I didn't even have to use the #ParamConverter annotation to make it work. The only other change I had to make to the controller was changing the method signature of my route to public function deletePost(Post $post) (as I had tried previously - but now works due to my PostConverter).
My final two questions would be:
What exactly should I check for in the supports() method? I'm currently just checking that the class matches. Should I also be checking that $configuration->getName() == "id", to ensure I'm working with the correct field?
How might I go about making it more generic? Am I correct in assuming that anytime you inject an entity in a controller method, Symfony will call the supports method on everything that implements ParamConverterInterface?
Thanks.
What would happen if you used Doctrine is that you'd need to type-hint your $post variable. After you've done that, Doctrine's ParamConverter would take care of the rest. Right now, Symfony has no idea how about how to related your id url placeholder to your $post parameter, because it doesn't know which Entity $post refers to. By type-hinting it with something like public function deletePost(Post $post) and using a ParamConverter, Symfony would know that $post refers to the Post entity with the id from the url's id placeholder.
From the doc:
Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.
The Voter would then also know what $post is and how to treat it.
Now since you are not using Doctrine, you don't have a ParamConverter by default, and as we just saw, this is the crucial element here. So what you're going to have to do is simply to define your own ParamConverter.
This page of the Symfony documentation will tell you more about how to do that, especially the last section "Creating a Converter". You will have to tell it how to convert the string "id" into a Post object using your model's logic. At first, you can make it very specific to Post objects (and you may want to refer to that one ParamConverter explicitly in the annotation using the converter="name" option). Later on once you've got a working version, you can make it work more generic.
I'm using ActiveForm with Yii2 and by default it seems to generate default id's for fields if you don't set one, in the format of:
{action-name}-{field-name}
So for example if I had a field with the name of foo_bar used in an action of actionSettings then the id of this field would be generated as:
settings-foo_bar
I would prefer this to just be foo_bar.
Is this possible to change on a form by form basis?
Based on the answer provided by #Bizley I was investigating how the method calculated the name and found out there is another way to achieve this as well.
You can simply override the formName method of your respective model to return a blank value, such as:
public function formName() {
return '';
}
Whilst this has less overheads as you don't need to create a new class, it will also affect other things within your form such as the field names and also should not be used for forms which contain multiple different models as explained here.
Lastly, because this question was about changing how Yii formats the id, #Bizleys answer is the correct one; my solution is just another option of possibly achieving it another way.
ActiveField id is by default created based on the form's model name and field's name.
If you want to change it for the whole form override the method that does it:
protected function getInputId()
{
return $this->_inputId ?: Html::getInputId($this->model, $this->attribute);
}
and use this modified class in your form.
I want to create readonly hidden field. Now I have field that looks like this:
$builder
->add('question_category_id', HiddenType::class);
And entity has method:
public function getQuestionCategoryId() {
return $this->getQuestion()->getQuestionCategory()->getId();
}
After saving I got following error:
Neither the property "question_category_id" nor one of the methods "addQuestionCategoryId()"/"removeQuestionCategoryId()", "setQuestionCategoryId()", "questionCategoryId()", "__set()" or "__call()" exist and have public access in class "Entity\UnitQuestionAnswer".
I could add dummy method
public function setQuestionCategoryId($id) {
return $this;
}
but it is not right way.
How to create readonly hidden field, or avoid of writing back data from from into entity?
S2.8 has a read_only attribute which would do what you want but it has been removed in 3.0.
The disabled attribute should work. Just be aware that the value itself will not actually be submitted symfony.com/doc/current/reference/forms/types/… so if you are doing anything funky with the posted data then that could be a problem.
I suppose it's possible to fool around with the internals but that would be more trouble than it is worth.
Personally, given that my get method was added just for the form, I would just add a corresponding set method and move on.
I'am using CakePhp3 for my website and I have to inject some custom validation logic based on the current user Id when I'am creating or modifying an entity.
The basic case is "Is the user allow to change this field to this new value" ? If' not, I want to raise a validation error (or an unauthorized exception).
In cakephp, for what I'am understanding, most of the application and businness rules must be placed on Models or 'ModelsTable'of the ORM. But, in this classes, the AuthComponent or the current session is not available.
I don't want to call manually a method on the entity from the controller each time I need to check. I would like to use a validator, something like :
$validator->add('protected_data', 'valid', [
'rule' => 'canChangeProtectedData',
'message' => __('You're not able to change this data !'),
'provider' => 'table',
]);
Method on ModelTable :
public function canChangeProtectedData($value, array $context)
{
\Cake\Log\Log::debug("canChangeProtectedData");
// Find logged user, look at the new value, check if he is authorized to do that, return true/false
return false;
}
I cakephp < 3, the AuthComponent have a static method 'AuthComponent::user()' that is not available anymore. So, how Can I do that in CakePhp 3 ?
Thank you for any response.
EDIT - Adding more details
So here are more details. In case of an REST API. I have an edit function of an entity. The "Article" Entity.
This Article has an owner with a foreign key on the column named "user_id" (nothing special here). My users are organized in groups with a leader on the group. Leaders of groups can change article's owner but "basics" users can't do it (but they can edit their own articles). Admin users can edit everything.
So the edit method must be available for any authenticated user, but changing the "user_id" of the entity must be allowed and checked depending the case (if I'am admin yes, if I'am leader yes only if the new Id is one of my group and if I'am basic user no).
I can do this check on the controller but if I want this rule to be checked everywhere in my code where an Article is modified (in another method than the "Edit" of ArticlesController). So for me the Model seems the good place to put it no?
Authentication vs Authorisation
Authentication means identifying an user by credentials, which most of the time boils down to "Is a user logged in".
Authorisation means to check if an user is allowed to do a specific action
So don't mix these two.
You don't want validation you want application rules
Taken from the book:
Validation vs. Application Rules
The CakePHP ORM is unique in that it uses a two-layered approach to
validation.
The first layer is validation. Validation rules are intended to
operate in a stateless way. They are best leveraged to ensure that the
shape, data types and format of data is correct.
The second layer is application rules. Application rules are best
leveraged to check stateful properties of your entities. For example,
validation rules could ensure that an email address is valid, while an
application rule could ensure that the email address is unique.
What you want to implement is complex application logic and more than just a simple validation, so the best way to implement this is as an application rule.
I'm taking a code snippet from one of my articles that explains a similar case. I had to check for a limitation of languages (translations) that can be associated to a model. You can read the whole article here http://florian-kraemer.net/2016/08/complex-application-rules-in-cakephp3/
<?php
namespace App\Model\Rule;
use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use RuntimeException;
class ProfileLanguageLimitRule {
/**
* Performs the check
*
* #link http://php.net/manual/en/language.oop5.magic.php
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #param array $options Options.
* #return bool
*/
public function __invoke(EntityInterface $entity, array $options) {
if (!isset($entity->profile_constraint->amount_of_languages)) {
if (!isset($entity->profile_constraint_id)) {
throw new RuntimeException('Profile Constraint ID is missing!');
}
$languageLimit = $this->_getConstraintFromDB($entity);
} else {
$languageLimit = $entity->profile_constraint->amount_of_languages;
}
// Unlimited languages are represented by -1
if ($languageLimit === -1) {
return true;
}
// -1 Here because the language_id of the profiles table already counts as one language
// So it's always -1 of the constraint value
$count = count($entity->languages);
return $count <= ($languageLimit - 1);
}
/**
* Gets the limitation from the ProfileConstraints Table object.
*
* #param \Cake\Datasource\EntityInterface $entity Entity.
* #return int
*/
protected function _getConstraintFromDB(EntityInterface $entity) {
$constraintsTable = TableRegistry::get('ProfileConstraints');
$constraint = $constraintsTable->find()
->where([
'id' => $entity['profile_constraint_id']
])
->select([
'amount_of_languages'
])
->firstOrFail();
return $constraint->amount_of_languages;
}
}
I think it is pretty self-explaining. Make sure your entities user_id field is not accessible for the "public". Before saving the data, just after the patching add it:
$entity->set('user_id', $this->Auth->user('id'));
If you alter the above snippet and change the profile_constraint_id to user_id or whatever else you have there this should do the job for you.
What you really want is row / field level based authorisation
Guess you can use ACL for that, but I've never ever had the need for field based ACL yet. So I can't give you much input on that, but it was (Cake2) and still is (Cake3) possible. For Cake3 the ACL stuff was moved to a plugin. Technically it is possible to check against anything, DB fields, rows, anything.
You could write a behavior that uses the Model.beforeMarshal event and checks if user_id (or role, or whatever) is present and not empty and then run a check on all fields you want for the given user id or user role using ACL.
You could probably use this method PermissionsTable::check() or you can write a more dedicated method does checks on multiple objects (fields) at the same time. Like I said, you'll spend some time to figure the best way out using ACL if you go for it.
UX and yet another cheap solution
First I would not show fields at all an user is not allowed to change or enter as inputs. If you need to show them, fine, disable the form input or just show it as text. Then use a regular set of validation rules that requires the field to be empty (or not present) or empty a list of fields based on your users role. If you don't show the fields the user would have to temper the form and then fail the CSRF check as well (if used).
I don't think you need to validate in the table. I just thought of a way to do it in the controller.
In my Users/Add method in the controller for instance:
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data);
//check if user is logged in and is a certain user
if ($this->request->session()->read('Auth.User.id') === 1) {
//allow adding/editing role or whatever
$user->role = $this->request->data('role');
} else {
$user->role = 4;//or whatever the correct data is for your problem.
}
if ($this->Users->save($user)) {
$this->Flash->success(__('You have been added.'));
} else {
$this->Flash->error(__('You could not be added. Please, try again.'));
}
}
$this->set(compact('user'));
$this->set('_serialize', ['user']);
}
I am using Symfony with propel to generate a form called BaseMeetingMeetingsForm.
In MeetingMeetingsForm.class.php I have the following configure method:
public function configure() {
$this->useFields(array('name', 'group_id', 'location', 'start', 'length'));
$this->widgetSchema['invited'] = new myWidgetFormTokenAutocompleter(array("url"=>"/user/json"));
}
In MeetingMeetings.php my save method is simply:
public function save(PropelPDO $con = null) {
$this->setOwnerId(Meeting::getUserId());
return parent::save($con);
}
However propel doesn't know about my custom field and as such doesn't do anything with it. Where and how to I put in a special section that can deal with this form field, please be aware it is not just a simple save to database, I need to deal with the input specially before it is input.
Thanks for your time and advice,
You have to define a validator (and/or create your own). The validator clean() method returns the value that needs to be persisted.
In Doctrine (I don't know Propel) the form then calls the doUpdateObject() on the form, which in turns calls the fromArray($arr) function on the model.
So if it's already a property on your model you'll only need to create the validator. If it's a more complex widget, you'll need to add some logic to the form.