Field resolver for InputObjectGraphType - asp.net

I work on a ASP.NET Web API and have added support for GraphQL requests with GraphQL for .NET .
My queries are working as expected but I am now struggling to use the same logic with the mutations.
I have the following logic for my queries:
Field<ContactType>("contact", "This field returns the contact by Id",
arguments: new QueryArguments(QA_ContactId),
resolve: ctx => ContactResolvers.ContactDetails(ctx));
My resolver returns a ContactDomainEntity that is then resolved into a ContactType:
public class ContactType : ObjectGraphType<ContactDomainEntity>
{
public ContactType()
{
Name = "Contact";
Description = "Contact Type";
Field(c => c.Id);
Field(c => c.FirstName, nullable: true);
Field<ListGraphType<AddressType>, IEnumerable<AddressDTO>>(Field_Addresses)
.Description("Contact's addresses")
.Resolve(ctx => LocationResolvers.ResolveAddresses(ctx));
}
}
It all works really well and the address list is resolved with its own reslver (LocationResolvers.ResolveAddresses) which makes it reusable and helps with the separation of concerns.
Now I want to be able to edit a contact and was hoping to use the same logic where child-objects (like the list of addresses) would be handled by their own resolver. So I have created the following mutation:
Field<ContactType>("UpdateContact", "This field updates the Contact's details",
arguments: new QueryArguments(QA_Input<Types.Input.ContactInputType>()),
resolve: ctx => ContactResolvers.UpdateContact(ctx));
with the ContactInputType:
public class ContactInputType : InputObjectGraphType<ContactInputDTO>
{
public ContactInputType()
{
Name = "UpdateContactInput";
Description = "Update an existing contact";
Field(c => c.Id);
Field(c => c.FirstName, nullable: true);
Field<ListGraphType<AddressInputType>, IEnumerable<AddressDTO>>("Addresses")
.Description("Manage contact's addresses")
.Resolve(ctx => LocationResolvers.ManageAddresses(ctx));
}
}
(Note that I use DTOs to map the fields into an object which makes sense in my case but that is not related to my problem)
My issue is that only the resolver 'ContactResolvers.UpdateContact' gets called. The field resolver 'LocationResolvers.ManageAddresses' is never hit. If I replace the addresses field with the following:
Field(c => c.Addresses, nullable: true, type: typeof(ListGraphType<AddressInputType>));
my ContactInputDTO is correctly populated (i.e. its property 'Addresses' contains the right data) but it means I lose control on how the object properties are mapped and have to rely on them having the same name and cannot add additional logic that my resolver might have.
tl;dr How to use a field resolver with InputObjectGraphType? It works fine when returning an ObjectGraphType but I can't get it working on the receiving end.

Related

Doctrine weird behavior, changes entity that I never persisted

I have this situation:
Symfony 4.4.8, in the controller, for some users, I change some properties of an entity before displaying it:
public function viewAction(string $id)
{
$em = $this->getDoctrine()->getManager();
/** #var $offer Offer */
$offer = $em->getRepository(Offer::class)->find($id);
// For this user the payout is different, set the new payout
// (For displaying purposes only, not intended to be stored in the db)
$offer->setPayout($newPayout);
return $this->render('offers/view.html.twig', ['offer' => $offer]);
}
Then, I have a onKernelTerminate listener that updates the user language if they changed it:
public function onKernelTerminate(TerminateEvent $event)
{
$request = $event->getRequest();
if ($request->isXmlHttpRequest()) {
// Don't do this for ajax requests
return;
}
if (is_object($this->user)) {
// Check if language has changed. If so, persist the change for the next login
if ($this->user->getLang() && ($this->user->getLang() != $request->getLocale())) {
$this->user->setLang($request->getLocale());
$this->em->persist($this->user);
$this->em->flush();
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::TERMINATE => [['onKernelTerminate', 15]],
];
}
Now, there is something very weird happening here, if the user changes language, the offer is flushed to the db with the new payout, even if I never persisted it!
Any idea how to fix or debug this?
PS: this is happening even if I remove $this->em->persist($this->user);, I was thinking maybe it's because of some relationship between the user and the offer... but it's not the case.
I'm sure the offer is persisted because I've added a dd('beforeUpdate'); in the Offer::beforeUpdate() method and it gets printed at the bottom of the page.
alright, so by design, when you call flush on the entity manager, doctrine will commit all the changes done to managed entities to the database.
Changing values "just for display" on an entity that represents a record in database ("managed entity") is really really bad design in that case. It begs the question what the value on your entity actually means, too.
Depending on your use case, I see a few options:
create a display object/array/"dto" just for your rendering:
$display = [
'payout' => $offer->getPayout(),
// ...
];
$display['payout'] = $newPayout;
return $this->render('offers/view.html.twig', ['offer' => $display]);
or create a new non-persisted entity
use override-style rendering logic
return $this->render('offers/view.html.twig', [
'offer' => $offer,
'override' => ['payout' => $newPayout],
]);
in your template, select the override when it exists
{{ override.payout ?? offer.payout }}
add a virtual field (meaning it's not stored in a column!) to your entity, maybe call it "displayPayout" and use the content of that if it exists

Symfony API : test if the content has the keys

With Symfony, I can get the content of request (POST and application/json) with :
$content = json_decode($request->getContent());
dd($content) return :
[
"subject" => "test"
"content" => "content"
]
I just want to hydrate an object with the subject, this works good :
$subject = new SubjectEntity();
$subject->setSubject($content['subject]);
But I don't want to do a setSubject() if the subject key doesn't exist.
I know I can do an if for each variable (Or $subject->setSubject($content['subject] ?? null)), but I think it will be a bit tedious in more complex cases.
Is there a way to "validate" the content sent to verify that all the desired keys are present ?
The cleanest solution would be a request param validator using symfony validator
Example:
public function validateRequest(array $params): array
{
$validator = Validation::createValidator();
$constraints = new Assert\Collection([
'subject' => new Assert\Type('string'),
'content' => new Assert\Type('string'),
]);
$group = new Assert\GroupSequence(['Default', 'custom']);
return $validator->validate($params, $constraints, );
}
If the keys can be optional, then you must use Assert\Optional(). Either way, you will need the null coalescing operator to cast to null if the key isn't set.
You can put these in a different class and with a small refactor you can make it work much nicer than doing a bunch of ifs, and it can be re-used to validate all of your api requests.

How to access Locale field values through ::get() using Silverstripe Fluent

We are working on a project using Silverstripe with the Fluent module to enable multiple translations.
Here's an example Data Object with Fluent enabled. First we create the Data Object & explicitly set the CMS fields:
namespace Yard;
use SilverStripe\ORM\DataObject;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
class Milkshake extends DataObject {
private static $table_name = 'Milkshake';
private static $db = [
'Title' => 'Varchar(255)'
]
public function getCMSFields() {
$fields = new FieldList(
new TextField('Title', 'Milkshake Title', null, 255)
);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Then we set Title as translatable in a YML file:
Yard\Milkshake:
extensions:
- 'TractorCow\Fluent\Extension\FluentExtension'
translate:
- 'Title'
This gives us an object with a translatable Title field that can have different values in different locales. It creates the following database table:
Milkshake_Localised
ID | RecordID | Locale | Title
So far so good, except using:
$milkshake = Milkshake::get()->first() doesn't return the localised data & pulls from the Milkshake table.
I think it could be possible to use:
$locale= FluentState::singleton()->getLocale();
$milkshake = Milkshake_Localised::get()->filter(['Locale' => $locale])->first();
But this feels clumsy & has no fallback if the locale data doesn't exist for that field (at which point it should fall back to the default locale, or failing that the original Milkshake field).
What is the correct way to access Locale data in Fluent so there is a fallback if required?
I got the desired behaviour by wrapping the get command in "withState"
use TractorCow\Fluent\State\FluentState;
$milkshake = FluentState::singleton()->withState(function (FluentState $state) {
return Milkshake::get()->first();
});

Laravel - multiple forms/tables - carry over variable in URL to fill table column

I am quite new to coding and to Laravel (using 5.7). I understood how to create a basic one-page form to populate a table in a database, including relashionship and authentication but I have difficulties reaching the next level.
The app I want to design will have multiple forms and tables and I can't figure out how to link info collected from the first form to the following one.
Let's consider a Database with:
- a Clients table (populated thru form page A) ex: one field is a client_id field.
- a Products table (populated thru form page B)
- (ultimately they will be more)
When the user (e.g. an employee of a compagny analyzing clients behavior) is done filling the form page A (URL: /clients, GET method: Clientcontroller#create, view clients.create.blade.php), he/she click Next.
My idea is that the Next button should:
- submit information to the Client table (POST method: Clientscontroller#store)
- and redirect the user to the page B of the form, carrying over the client_id in the URL (URL: /products/{client_id}/create, GET method: Productscontroller#create, view products.create.blade.php).
In the Product table, I have a client_id column and I have a one to many relashionship between the Clients and Products model.
Upon submission of form B, I would like to retrieve the {client_id} from URL to fill the client_id column of the Product table but I am stuck here. I would appreciate pieces of guidance and advices. For simplification during the learning process, I consider that Clients only buy one product.
THE MAIN QUESTION IS:
- How to retrieve the {client_id} parameter from the URL of the products.create.blade.php view to inject it onto the client view (already tried a lot of thing from answer to similar questions in stackoverflow)
ALSO:
- Am I using the right approach? Any suggestions/advices?
EXTRA QUESTION a bit out of the scope:
- Any hint on how implement add/remove fields for products?
WEB ROUTES FILE:
> <?php
> Route::get('/', 'PagesController#welcome')->name('welcome');
> Auth::routes();
> //ROUTES FOR CLIENT
> Route::resource('clients','ClientsController');
> //ROUTES FOR PRODUCT (please note the {client_id} parameter)
> Route::get('/products/{client_id}/create', 'ProductsController#create')->name('products.create');
> Route::post('/products/{client_id}', 'ProductsController#store')->name('products.store');
> //not sure if it should be Route::post('/products', 'ProductsController#store')->name('products.store');
CLIENTS CONTROLLER:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Client; //THIS IS THE CLIENT MODEL
use App\User; ////THIS IS THE USER MODEL
class ClientsController extends Controller
{
AUTH
public function __construct()
{
$this->middleware('auth');
}
//INDEX (CLIENT)
public function index()
{
//my code here
}
//CREATE (CLIENT)
public function create()
{
$datas=[
'controllermethod' => 'ClientsController#store',
'client_id_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'client_id', //this what i am carrying over in URL
'input_label_l' => 'Client ID :',
'input_placeholder_l' => 'XXXXX',
'input_default_l' => ''
],
'client_info_1_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'client_info_1'
'input_label_l' => 'Client Info 1 :',
'input_placeholder_l' => 'XXXXX',
'input_default_l' => ''
],
'client_info_2_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'client_info_2'
'input_label_l' => 'Client Info 2 :',
'input_placeholder_l' => 'XXXXX',
'input_default_l' => ''
]
],
return view ('clients.create')->with($datas);
}
//STORE (CLIENT)
public function store(Request $request)
{
$this->validate($request, [
'client_id' => 'required',
]);
$client = new Client;
$client->client_id = $request->input('client_id');
$client->client_info_1 = $request->input('client_info_1');
$client->client_info_2 = $request->input('client_info_2');
$client->user_id = auth()->user()->id; //one to many relashionship betw user/client models
$client->save();
//
//this is how I inject the {client_id} parameter onto the URL
//this works, the products.create view is displayed, the URL contain the client_id from formA entered by the user
$client_id = $request->client_id;
return redirect("/products/$client_id/create")->with('client_id', $client_id)
->with('success','New client record created');
//SHOW/DESTROY/EDIT/UPDATE FUNCTIONS....all this work
PRODUCTS CONTROLLER = THAT's WERE I AM STUCK + NOT SURE IF IT's THE RIGHT APPROACH
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
//note sure if I need that
//use Illuminate\Routing\Route;
use App\Product;
use App\Client;
use App\User;
class ProductsController extends Controller
{
//AUTH
public function __construct()
{
$this->middleware('auth');
}
//INDEX (PRODUCT)
public function index()
{
//no code so far
}
//INDEX (PRODUCT)
public function create()
{
$datas=[
'controllermethod' => 'ProductsController#store',
//planning of having dynamic add/remove products fields but let's keep it simple (1 client -> 1 product)
'product_id_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'product_id',
'input_label_l' => 'Date of cancer diagnosis :',
'input_default_l' => ''
],
'product_info_1_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'product_info_1'
'input_label_l' => 'Product Info 1 :',
'input_placeholder_l' => 'XXXXX',
'input_default_l' => ''
],
'product_info_2_field' => [
'input_type_l' => 'textinput',
'input_name_l' => 'product_info_2'
'input_label_l' => 'Product Info 2 :',
'input_placeholder_l' => 'XXXXX',
'input_default_l' => ''
]
],
//Below, I am not sure I should do that
return view ('products.create')->with($datas);
}
// STORE (PRODUCT) = THAT's WHERE I AM STUCK
// everything works except that the client_id column in the products table stays empty or filled with crap
public function store(Request $request)
{
//NOT SURE WHAT TO DO HERE TO RETRIEVE THE {client_id} from the "/products/$client_id/create" ...
//TO FURTHER INJECT IT AS A VALUE IN THE client_id COLUMN OF THE PRODUCT TABLE
//I KNOW IT's CRAP, but I TRIED THINGS ALONG THOSE LINES:
//return $client_id;
//$client_id = request()->route()->paremeters('client_id');
//$client_id = request()->route('client_id');
//$client_id = $request->client_id;
//$client_id = url('client_id');
$product = new Diagnosis;
$product->product_id = $request->input('product_id');
$product->product_info_1 = $request->input('product_info_1');
$product->product_info_2 = $request->input('product_info_2');
$product->user_id = auth()->user()->id;
//I KNOW IT's CRAP, but I TRIED THINGS ALONG THOSE LINES:
//$diagnosis->client_id = $client_id; //NB: if I write $diagnosis->client_id = 'whatever'; it works
$diagnosis->save();
//redirect to client.index view
return redirect('/clients')->with('success','New patient diagnosis (-es) created');
}
our solution will start by looking to this route :
Route::get('/products/{client_id}/create','ProductsController#create')->name('products.create');
this is the route that contains your client_id that we want to retrieve, and since this value is on the link, we can use the Request object to access to it!
like i said, the $request object already contains the client_id value, so what we need is to retrieve it and send it as a parameter to the view using the with function, so basically your ProductsController#create will be like this :
public function create(Request $request)
{
// all your code ...
return view ('products.create')->with("data",$datas)->with("client_id",$request->client_id);
}
so now, we access this value from our view right ? the idea is to add this value as a hidden input on the product form! ( something like this )
<form>
<!-- other inputs .... -->
<input type="hidden" value="{{$client_id}}" name="client_id" />
</form>
after submitting the form, this route will be called :
Route::post('/products/{client_id}', 'ProductsController#store')->name('products.store');
since the client_id will be sent through the form and not using the link, it would be better if you change your route to
Route::post('/products', 'ProductsController#store')->name('products.store');
now, we can have access to our value on the store function just by using
$client_id = $request->input('client_id')
Here is an alternative solution which works too:
ROUTES:
Route::get('/products/{client_id}/create', 'ProductsController#create')-name('products.create');
Route::post('/products', 'ProductsController#store')->name('products.store');
PRODUCTS CONTROLLER CREATE FUNCTION:
public function create() //keep as in the original question
{
// all your code ...
return view ('products.create')->with("data",$datas)->with("client_id",$request->client_id);
}
IN THE VIEW:
<form>
<!-- other inputs .... -->
<input type="hidden" value="{{Request::segment(2)}}" name="client_id" /> //access the URL segmment 2 which correspond to the client_id
</form>
IN THE PRODUCTS CONTROLLER STORE FUNCTION:
$client_id = $request->input('client_id')
It would have be nice to use the Request::segment(2) directly in the store function like this but this triggers a "Non-static method Illuminate\Http\Request::segment() should not be called statically" error that I have not been able to resolve. I don't need an answer to that, however an hint would be appreciated anyway. Maybe I'll post a new question if this has not been resolved already.

How to add custom property to Symfony Doctrine YAML mapping file

Can anyone tell me how to add custom property to doctrine ORM yml file?
My idea is to add a property like this:
fields:
name:
type: string
localizable: true
Then I would like to get information about this localizable property by using
protected function getEntityMetadata($entity)
{
$factory = new DisconnectedMetadataFactory($this->getContainer()->get('doctrine'));
return $factory->getClassMetadata($entity)->getMetadata();
}
and then:
$met = $this->getEntityMetadata($bundle.'\\Entity\\'.$entity);
$this->metadata = $met[0];
$fields = $this->metadata->fieldMappings;
if (isset($fields)) {
foreach ($fields as $field => $fieldMapping) {
if (isset($fieldMapping['localizable']) && $fieldMapping['localizable'] == true) {
// Do sth with it
}
}
}
The way doctrine is written makes this awkward. It seems like you'd like to keep the Yaml mapping but just add a single property. I think you can create your own custom driver extending from the one provided. The Yaml driver has mostly private methods so overriding a little bit of the functionality is difficult, but it is possible.
I created a custom driver that extends from the SimplifiedYamlDriver. The naming of the driver is important because doctrine extension will try to load one of their drivers based what comes before Driver. It also does a strpos check for Simplified in the name, so I think the safest bet is to keep the original name completely and give the original an alias.
use Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver as BaseDriver;
class SimplifiedYamlDriver extends BaseDriver
{
public function loadMetadataForClass($className, ClassMetadata $metadata)
{
parent::loadMetadataForClass($className, $metadata);
$element = $this->getElement($className);
if (!isset($element['fields'])) {
return;
}
foreach ($element['fields'] as $name => $fieldMapping) {
if (isset($fieldMapping['localizable'])) {
$original = $metadata->getFieldMapping($name);
$additional = ['localizable' => $fieldMapping['localizable']];
$newMapping = array_merge($original, $additional);
$metadata->fieldMappings[$newMapping['fieldName']] = $newMapping;
}
}
}
}
Then I told Symfony to use this driver by overriding the class inside app/config/parameters.yml
parameters:
doctrine.orm.metadata.yml.class: MyBundle\SimplifiedYamlDriver
Then I updated the mapping like in your example inside MyBundle/Resources/config/doctrine/Foo.orm.yml
MyBundle\Entity\Foo:
type: entity
id:
id:
type: integer
generator:
strategy: IDENTITY
fields:
text:
type: string
localizable: true
And I can fetch this mapping wherever I have access to doctrine with:
$mapping = $this
->getDoctrine()
->getEntityManager()
->getClassMetadata(Foo::class)
->getFieldMapping('text');
Will give me:
Array
(
[fieldName] => text
[type] => string
[columnName] => text
[localizable] => 1
)
Unfortunately, this is impossible without rewriting a significant part of Doctrine DBAL. This would impact drivers (YAML, annotation...), meta data generator...
In your case, the simplest I see would be to add a custom type let's say LocalizableString (I guess at most you will need that and maybe LocalizableText).
Adding a type is relatively straightforward, since you can extend a base type so you don't have to write any SQL. You can refer to Doctrine documentation here and Doctrine bundle one here.
Then you can just do:
$met = $this->getEntityMetadata($bundle.'\\Entity\\'.$entity);
$this->metadata = $met[0];
$fields = $this->metadata->fieldMappings;
if (isset($fields)) {
foreach ($fields as $field => $fieldMapping) {
if ($this->getClassMetadata()->getTypeOfField($field) === 'localized_string') {
// Do sth with it
}
}
}

Resources