Create new type in symfony2 - symfony

How can I create FormType in Symfony2 for converting an entity to a string and back?
I've done all that is saying in here but there is an error:
Expected argument of type "string", "<Vendor>\<Bundle>\Entity\User" given
How can I create a form where a text field will be converted to an user object?

Assuming User has an username field i would do a transform like the following. Please pay attention that transform is for User to string transform, while reverseTransform is the opposite.
Add the transformer to your form field:
$builder
->add('user', 'text')
->addViewTransformer($transformer)
Relevant code (like example you've cited):
/**
* Transforms an User to a string.
*
* #param User|null $user
* #return string
*/
public function transform($user)
{
return $user ? $user->getUsername() : '';
}
/**
* Transforms a string to an User.
*
* #param string $username
* #return User|null
*/
public function reverseTransform($username)
{
if(empty($username)) return null;
$user = $this->om
->getRepository('AcmeHelloBundle:User')
->findOneBy(array('username' => $username))
;
return $user; // Can be null
}

You can extract this form type here and use that. https://github.com/symfony/symfony/pull/1951 it does what you are asking.

Related

Handling imporper data during deserialization when using Symfony Serializer Component

I am new to the Symfony serializer component. I am trying to properly deserialize a JSON body to the following DTO:
class PostDTO
{
/** #var string */
private $name;
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
}
The controller method as follows:
/**
* #Route (path="", methods={"POST"}, name="new_post")
* #param Request $request
* #return Response
*/
public function create(Request $request): Response
{
$model = $this->serializer->deserialize($request->getContent(), PostDTO::class, 'json');
// call the service with the model
return new JsonResponse();
}
My problem is that I wanted to handle business-validation after the body was deserialized. However, if i specify an invalid value for the name, such as false or [], the deserialization will fail with an exception: Symfony\Component\Serializer\Exception\NotNormalizableValueException: "The type of the "name" attribute for class "App\Service\PostDTO" must be one of "string" ("array" given)..
I do understand that it is because I intentionally set "name": []. However, I was looking for a way to set the fields to a default value or even perform some validation pre-deserialization.
I have found the proper way to handle this. That exception was thrown because the serializer was not able to create the PostDTO class using the invalid payload I have provided.
To handle this, I have created my custom denormalizer which kicks in only for this particular class. To do this, I have implemented the DenormalizerInterface like so:
use App\Service\PostDTO;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PostDTODeserializer implements DenormalizerInterface
{
/** #var ObjectNormalizer */
private $normalizer;
/**
* PostDTODeserializer constructor.
* #param ObjectNormalizer $normalizer
*/
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
return $type === PostDTO::class;
}
/**
* #param mixed $data
* #param string $type
* #param string|null $format
* #return array|bool|object
* #throws ExceptionInterface
*/
public function supportsDenormalization($data, string $type, string $format = null)
{
// validate the array which will be normalized (you should write your validator and inject it through the constructor)
if (!is_string($data['name'])) {
// normally you would throw an exception and leverage the `ErrorController` functionality
// do something
}
// convert the array to the object
return $this->normalizer->denormalize($data, $type, $format);
}
}
If you want to access the context array, you can implement the DenormalizerAwareInterface. Normally, you would create your custom validation and inject it into this denormalizer and validate the $data array.
Please not that I have injected the ObjectNormalizer here so that when the data successfully passed the validation, I can still construct the PostDTO using the $data.
PS: in my case, the autowiring has automatically registered my custom denormalizer. If yours is not autowired automatically, go to services.yaml and add the following lines:
App\Serializer\PostDTODeserializer:
tags: ['serializer.normalizer']
(I have tagged the implementation with serializer.normalizer so as it is recognized during the deserialization pipeline)

Laravel 5.8 Custom Email and password Columns

I have a WordPress running application, which I would like to access using a separate interface that uses Laravel 5.8.(don't worry about the hashing)
As such, instead of cloning passwords back and forth, I would like to use the user_email and user_pass columns in the Laravel User model instead.
I have tried what the official docs say :
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* Handle an authentication attempt.
*
* #param \Illuminate\Http\Request $request
*
* #return Response
*/
public function authenticate(Request $request)
{
$credentials = $request->only('user_email', 'user_pass');
if (Auth::attempt($credentials)) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}
I then edited the blade files, but no avail. Any pointers?
Laravel provides a way to change the default columns for auth (email, password) by overriding some functions.
In your User model add this function that overrides the default column for password:
App/User.php
/**
* Get the password for the user.
*
* #return string
*/
public function getAuthPassword()
{
return $this->user_pass;
}
And, in your LoginController change from email to user_email
App/Http/Controllers/Auth/LoginController.php
/**
* Get the login username to be used by the controller.
*
* #return string
*/
public function username()
{
return 'user_email';
}
Now you have overridden the default columns used by Laravel's Auth logic. But you are not finished yet.
LoginController has a function that validates the user's input and the password column is hardcoded to password so in order to change that, you also need to add these functions in LoginController:
App/Http/Controllers/Auth/LoginController.php
/**
* Validate the user login request.
*
* #param \Illuminate\Http\Request $request
* #return void
*
* #throws \Illuminate\Validation\ValidationException
*/
protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => 'required|string',
'user_pass' => 'required|string',
]);
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(Request $request)
{
return $request->only($this->username(), 'user_pass');
}
Next step is to create a custom Provider, let's call it CustomUserProvider that will be used instead of the default one EloquentUserProvider and where you will override the password field.
App/Providers/CustomUserProvider.php
<?php
namespace App\Providers;
class CustomUserProvider extends EloquentUserProvider
{
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('user_pass', $credentials))) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'user_pass')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} else {
$query->where($key, $value);
}
}
return $query->first();
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['user_pass'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
}
Now that you extended the default provider you need to tell Laravel to use this one instead of EloquentUserProvider. This is how you can do it.
App/Providers/AuthServiceProvider.php
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
$this->app->auth->provider('custom', function ($app, $config) {
return new CustomUserProvider($app['hash'], $config['model']);
});
}
Finally update the config information config/auth.php and change the driver from eloquent to custom (that's how I named it above; you can change it to whatever you want). So the config/auth.php file should have this bit:
'providers' => [
'users' => [
'driver' => 'custom',
'model' => App\User::class,
],
],
Hope it helps!
Regards
It would be up and working, If you can just use sessions here instead of using Auth::attempt just like working on core PHP.

Symfony2 Form with custom FormType calls DataTransformer with same data in both directions

I have made a new FormType and it extends the entity type via
//...
public function getParent()
{
return 'entity';
}
Which lead my edit form to complain that an integer was not My/Entity/Type and I need a data transformer. So I created one. This is the abbreviated version (it's just the basic tutorial version)
//...
public function reverseTransform($val)
{
// Entity to int
return $val->getId();
}
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
//...
Then added it to my form type
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new IdToMyModelTransformer($this->em));
}
This fixed me viewing my form, but now when I submit the form with an entity picked from my custom widget it tries to call transform not reverseTransform with the $val as an int the ->getId() fails on a non-object.
I can't figure out the correct way of doing this. If I use 'choice' as my widget parent I get a different set of issues (choice default constraints triggered saying it is invalid data?)
I need an entity passed to my widget so it can extract the meta data for display, but I can't post an entity back of course. How do I tell the form that?
Tried setting 'data_class' => null but no joy. Checking network tab shows the value is sent correctly when posting the form.
Update 1
So I re-read the DataTransformer page and that diagram got me thinking, especially after rubber-duck programming above, I ask the form for Entity but expect it to receive ints.. so I actually need a unidirectional transformer, ViewTransformer -> Get entity for display, get posted an int from widget, don't transform it just pass straight through. Which works and I just get the "invalid data" error on update.
Now I have in my Transformer:
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Update 2
That seems to have fixed it now, although for some reason if I post int 2 in my form the string "2/" is sent to my transformer. Any ideas on that? FOr now I'm cleaning the string in transformer, but seems like it just shouldnt be happening.
By what I'm seeing in your transformer class you're not implementing the code right. This should be the correct implementation:
namespace App\YourBundle\Form\DataTransformer;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IdToMyModelTransformer implements DataTransformerInterface
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em) {
$this->em = $em;
}
/**
* Transforms a value from the original representation to a transformed representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the datasource (object or array).
* 2. When data from a request is submitted using {#link Form::submit()} to transform the new input data
* back into the renderable format. For example if you have a date field and submit '2009-10-10'
* you might accept this value because its easily parsed, but the transformer still writes back
* "2009/10/10" onto the form field (for further displaying or other purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* #param mixed $object The value in the original representation
*
* #return mixed The value in the transformed representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function transform($object) {
if (null === $object) {
return null;
}
return $object->getId();
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {#link Form::submit()} is called to transform the requests tainted data
* into an acceptable format for your data processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as empty strings). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* #param mixed $categoryId The value in the transformed representation
*
* #return mixed The value in the original representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($id) {
if (!$id || $id <= 0) {
return null;
}
if(!ctype_digit($id)){
throw new TransformationFailedException();
}
$repo = $this->em->getRepository('...');
$result = $repo->findOneBy(array('id' => $id));
if (null === $result) {
throw new TransformationFailedException(
sprintf(
'Entity with id does not exist!',
$id
)
);
}
return $result;
}
}
In the IdToMyIntType you would have something like this:
namespace App\YourBundle\Form\Type;
use App\YourBundle\Form\DataTransformer\IdToMyModelTransformer ;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IdToMyModelType extends AbstractType {
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct( EntityManager $em ) {
$this->em = $em;
}
public function buildForm( FormBuilderInterface $builder, array $options ) {
$transformer = new IdToMyModelTransformer ( $this->em );
$builder->addModelTransformer( $transformer );
}
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array('invalid_message' => 'Something went wrong message.'));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'id_to_model_type';
}
}
I would suggest you check out the DataTransformerInterface and read the documentation over the methods. It'll briefly explain what is that method expected to do. Also, in case you have problems implementing it, you can always check the official documentation, which contains a working example and build up from there.
As per my last update I realised, because I was only using my form data to display the currently saved entity relation (the rest is provided by ajax) and not in the same format the form would be receiving it in it lead to some confusion.
To follow the tutorials wording:
Model data
This was all to remain as-is (No model datatransformer needed)
Norm data
No changes
View data (unidirection transformation required)
Transform()
ID to Entity so widget can access other properties
ReverseTransform()
Posted ID is in correct format so we just return it
Code
Very simplified:
private $om;
public function __construct (ObjectManager om)
{
$this->om = $om;
}
public function transform($val)
{
// Int to Entity
return $om->getRepository('MyBundle:EntityName')->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Hopefully that helps anyone else who lets their requirements confuse them!

Convert json encoded String to json object in FosRestBundle

I have a json encoded string in one of my DB fields, e.g.
[{"name":"car","price":"10"}]
I'm using the FosRestBundle to return the DB-Values in json format and the String above is returned - as String - nothing special until here;)
How can this String be converted, so that a Json Object is returned instead?
Finally I found the solution.
My Entity contained this:
/**
* #var string
*
* #ORM\Column(name="options", type="string", nullable=true)
*/
private $options;
"options" contained the json encoded string. So I tried it with the JMS Serilizer Annotation #Accessor and wrote this specific getter:
/**
* Get optionsAsArray
*
* #return array
*/
public function getOptionsAsArray()
{
return (array)json_decode($this->options, true);
}
Still got an error "Array to string conversion". So the Solution was, to add another Annotation #type and the JMSSerializer returned nicely formatted JSON.
This is what the Entity has to look like:
use JMS\Serializer\Annotation\Accessor;
use JMS\Serializer\Annotation\Type;
/* ... */
/**
* #var string
*
* #ORM\Column(name="options", type="string", nullable=true)
* #Accessor(getter="getOptionsAsArray")
* #Type("array")
*/
private $options;
You can decode this string to an stdClass or an associative array with json_decode. Is this what you are looking for?
EDIT : This should work
public function myAction()
{
// do stuff
$string = '[{"name":"car","price":"10"}]';
$array = json_decode($string, true);
/* array is like
[
0 => [
'name' => string(3) "car"
'price' => string(2) "10"
]
]
*/
return $array;
}

Symfony2, Doctrine, underscore in "findXxx" method name

Simple example, we've got
/**
* #ORM\Column(name="api_keyID", type="integer", nullable=false)
*/
private $api_keyID;
/**
* #return integer
*/
public function getApi_keyID()
{
return $this->api_keyID;
}
/**
* #param integer $api_keyID
* #return object
*/
public function setApi_keyID($data)
{
$this->api_keyID = $data;
return $this;
}
Look at method name and column name. When i try
//...
->findOneByApi_keyID($some);
I'm getting an error like
Entity 'entity\path' has no field 'apiKeyID'. You can therefore not call 'findOneByApi_keyID' on the entities' repository
So doctrine\symfony eats underscore? О.о And i cannot use it in column name?
is the way out
$repository->findBy(array('is_enabled' => true));
Founded here
Magic Doctrine2 finders when field has underscore?

Resources