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

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();
});

Related

Export user roles with Sonata admin

I'm using SonataAdmin and FosUserBundle with Symfony 4.
I want to use the export feature to export whole users' data in CSV, JSON ...
When a trigger the export, the roles column in the file is empty or null.
In the UserAdmin class, I have overridden the getExportFields function with the call of a specific method to get the role as explained in this post. Sonata admin export fields with collection fields
But it doesn't work.
Example in my case:
public function getExportFields()
{
return [
'id',
'username',
'roles' => 'rolesExported'
];
}
And in my User Entity:
public function getRolesExported()
{
$exportedRoles = [];
foreach ($this->getRealRoles() as $role) {
$exportedRoles[] = $role->__toString();
}
return $this->rolesExported = implode(' - ', $exportedRoles);
}
In this case, when I trigger the export, my web browser shows the error
'website is inaccessible' with no error in the dev.log.
When I delete 'roles' => 'rolesExported' in the getExportFields function, the export is well triggered.
SonataAdmin version : 3.35
FosUserBundle version : 2.1.2
Symfony version: 4.3.2 (I know that I need to update it)
I suspect that the __toString() call causes a problem.
although the post you used as your inspiration explicitly says that it wants to export collections, I assume you might want to export an array.
Since I don't know the type of your $role objects, for debugging purposes first replace $role->__toString() with gettype($role), so the line is:
$exportedRoles[] = gettype($role);
I see three cases here:
object or for multiple roles object - object - ..., in that case, you should select a method of Role that returns a proper string or create one at that place, like $exportedRoles[] = $role->getName();
string or for multiple roles string - string - ..., in that case your "real" roles is just an array, and you can replace the contents of your function with return implode(' - ', $this->getRealRoles());
array or for multiple roles array - array - ..., in that case you have an array for each role, and those don't provide __toString. Select a method to construct the exported role, like $exportedRoles[] = $role['name'];

Field resolver for InputObjectGraphType

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.

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
}
}
}

Date field type default only when no object is passed

I'm trying my hand at my first Symfony application, but as I'm working with forms, something a little counterintuitive is happening.
As you can see from my code, I have a date field that defaults to the current day. But when I pass an object to the form, this default overrides the object's current date.
I know this is how it is supposed to happen ('The default values for form fields are taken directly from the underlying data structure (e.g. an entity or an array). The data option overrides this default value.', from http://symfony.com/doc/current/reference/forms/types/date.html#data).
Is there any way to suppress this behaviour and only show the default if there is NO object passed?
$builder
// other code
->add('date', 'date', array(
'data' => new \DateTime()
))
// other code
I would probably set it directly in my new Entity, not fixed in a form
class YourClass
{
private $date;
//...
public function __construct()
{
$this->date = new \DateTime;
}
}

SilverStripe GridField: too many versions of DataObject get created

TL;DR When creating/saving a versioned DataObject with relation to some page, two entries are created in the corresponding versions table (instead of one).
I'm trying to version some DataObject and have the Versioned extension applied as follows:
class Testobject extends DataObject {
static $has_one = array(
'Page' => 'Page'
);
static $extensions = array(
"Versioned('Stage', 'Live')",
);
Testobjects are being managed in a GridField on some page like so:
class PageContent extends Page {
public static $has_many = array(
"Testobjects" => "TestObject"
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$gridField = new GridField(
'Testobjects',
'Testobject',
$this->Testobjects(),
$config);
$fields->addFieldToTab('Root.Main', $gridField);
}
Now, whenever i add or save some Testobject in the GridField's EditForm, two new entries show up in the Testobject_versions table. For comparsion, when i save a page in the SiteTree, only one entry in the corresponding versions table is created.
As there will we thousands of these DataObjects on a page, i'm worried about this duplication filling up my database. Is there a way to get around this?
Further recognitions:
On creation of a new Testobject, the first entry in the versions table has it's PageID field set to 0, the second entry has set the actual PageID of the corresponding page.
If I replace $this->Testobjects() in the GridField construction by Testobject::get(), only one entry shows up in the versions table.
onBeforeWrite is called twice when using $this->Testobjects()
So it seems setting the relation to the page happens after a first 'write()', then another 'write()' is called. But where in the code does this happen?
If you're editing your page/testobject in the main section of the CMS (# '/admin/pages'), you can try this somewhat hackish trick in your TestObject class
public function getCMSFields(){
$fields = parent::getCMSFields();
$fields->push( new HiddenField('PageID','PageID', Controller::curr()->CurrentPageID());
return $fields;
}
This is not ideal for the following reasons:
hard to test with unit test controller
awareness of the controller in the model is bad
IMHO
But it can be a reasonable fix if it works for you

Resources