Many DataObjects with has_many relationship to one generic DataObject - silverstripe

I want to create a class that extends DataObject and simply has a Title, Desc, and Image.
class ImageBlock extends DataObject
{
private static $db = [
'Title' => 'Varchar(50)',
'Description' => 'Varchar(255)'
];
private static $has_one = [
'Image' => 'Image'
];
}
This is generic Tile to display on the Frontend and could be shown on multiple pages and within multiple DataObjects. A given page or DO can have many of these. To clarify, this is not just for pages. I have a Region DO that has_many of these ImageBlocks:
class TourRegion extends \DataObject
{
private static $db = [
'RegionName' => 'Varchar(50)',
'RegionSlug' => 'Varchar(50)',
'RegionIntro' => 'Varchar(255)',
'RegionDescription' => 'Text',
];
private static $has_many = [
'RegionHeroImages' => 'TourHeroImage',
'MainAttractions' => 'ImageBlock'
];
....
My question is, a has_many to a DataObject requires a has_one relationship on that DataObject. Since the has_one relationship could be more than one possible class, how do I create this reference?
I have tried adding a has_one to the lowest common class that these objects share (DataObject) as follows:
class ImageBlock extends DataObject
{
private static $db = [
'Title' => 'Varchar(50)',
'Description' => 'Varchar(255)'
];
private static $has_one = [
'Image' => 'Image',
'ParentObject' => 'DataObject'
];
}
But I get this error:
[User Error] Uncaught Exception: No has_one found on class
'ImageBlock', the has_many relation from 'TourRegion' to
'ImageBlock' requires a has_one on 'ImageBlock'
I get the same error when I try to omit this has_one on ImageBlock altogether. Which begs the question; Why is it I can add has_many relationships to DataObjects like Image or File without the Image or File class having a has_one reference to my Object?
It seems that it's not possible to have generic and reusable has_many related objects in Silverstripe. And that every class that needs to have this ImageBlock must duplicate the class for the sole purpose of adding the has_one reference.

To answer the last part of your question, it's important to remember that has_many is schematically meaningless. It imposes no structural changes to your database. All it does is add a magic method to the parent DataObject that looks for a has_one somewhere else. Defining a has_many is basically just for convenience, to save you the time of writing a getter.
If you're looking to define the relation in the parent, which to me makes sense, I would do that as a many_many, as that requires no reciprocity (It can be reciprocated by belongs_many_many, but that is just a convenience method, too).
For consistency and clarity, I would create an extension to inject the many_many => ImageBlock to the DO's that want it.

Related

How to get has_many objects in DataExtension object?

SS4.4
I have two classes Member and Activity. A member has many activities. I have a class MemberExtension which extends Member. Inside MemberExtension, we have a has_many array containing ‘Activities’. How do we get the list of Activities in MemberExtension?
We have tried the following:
$this->Activities()
Error: Uncaught Error: Call to undefined method MemberExtension::Activities()
$this->getOwner()->Activities()
Error: Uncaught BadMethodCallException: Object->__call(): the method 'Activities' does not exist on 'SilverStripe\Security\Member'
We also ran dev/build?flush
// MemberExtension class
class MemberExtension extends DataExtension {
public static $has_many = [
'Activities' => Activity::class
];
}
// Activity class
class Activity extends DataObject {
private static $has_one = [
'Member' => Member::class,
];
}
// Register MemberExtension in _config.php
Member::add_extension(MemberExtension::class);
In the MemberExtension $this->owner->Activities() should work.
In Silverstripe CMS 4 you also need to use the FQCN (fully qualified class name, aka including the whole namespace. This means you need to use SilverStripe\Security\Member when adding the extension. It's also a good practice to use yml config files for adding extensions to classes, see documentation.
SilverStripe\Security\Member:
extensions:
- MemberExtension
assuming your extension doesn't have a namespace yet.
Then after running dev/build/flush you should be able to call the Activities relation as shown above:
$activities = $this->owner->Activities(); //name of the relation as always

Can I manage a has_one relationship with Gridfield (or similar) in Silverstripe?

We have an object with a has_one relationship with a secondary object in a Silverstripe project. The secondary object has multiple has_one fields
class IceCream extends DataObject
{
private static $has_one = [
'Cone' => 'Cone'
]
}
class Cone extends DataObject
{
private static $has_one = [
'Size' => 'Size',
'Pattern' => 'Pattern'
]
}
We want to be able to edit the secondary object Cone from the IceCream object in the CMS. Both creating new Cone records or linking existing Cone records.
If Cone was in a many_many relationship we could use Gridfield with the symbiote/silverstripe-gridfieldextensions module. And use the GridFieldAddExistingSearchButton & GridFieldAddNewInlineButton extensions.
Is there anyway to use this sort of behaviour for a has_one relationship?
I've investigated using the stevie-mayhew/hasoneedit module but it only appears to enable inline editing of has_one fields (in this example Size & Pattern) & doesn't appear to allow the user to link existing Cone records.
How can we create a field in the CMS that allows the user to link or create has_one records - preferably inline or as a modal?
There are a couple of modules you can use to do this.
Our preference is https://github.com/satrun77/silverstripe-hasoneselector, some other community members prefer to use https://github.com/silvershop/silverstripe-hasonefield, which is also a dependency of quite widely used https://github.com/gorriecoe/silverstripe-linkfield. Might be a better option in terms of support.

Get a bundles object/instance

How would I go about obtaining the instance of a specific bundle?
In trying to adhere to this best practice:
http://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options
I would like to stick some configuration details in a global bundle as they are likely to rarely change but are not specific to any given entity or bundle. Multiple bundles from multiple entities can use this to pre-populate a "Unit of Measure" drop down.
So I have done something like:
class ZincBundle extends Bundle {
private $units = [
'PCS' => 'PCS',
'LOT' => 'LOT',
'LBS' => 'LBS',
'SET' => 'SET',
'EACH' => 'EACH'
];
public function getUnitsOfMeasure ()
{
return $this->units;
}
}
Now I am trying to access this to populate some forms - this is what I have:
$container = $this->getConfigurationPool()->getContainer();
//$bundle = $this->container->get('ZincBundle');
What am I missing???

SS3 DataObject sort after specified DataObject

Ok, so this has been driving me crazy for the past couple days...
I have a DataObject like so:
private static $db = array(
// other properties
'active' => 'Boolean',
'sort' => 'Int' // ID of another DataObject of this type, after which it is to be displayed.
);
private static $has_one = array('Page' => 'CustomPage');
The CustomPage just extends Page and has a has_many relationship with this DataObject.
The pain now for me is to get the data in a way that they're correctly sorted.
EDIT: The sort value is actually the ID of the DataObject after which to sort this one.
For example given the following:
ID sort
1 0
2 3
3 5
4 1
5 1
The result should be ordered like this:
ID
1
4
5
3
2
The sort can be duplicated, since I don't really want to bother with updating every item whenever I just add something in the middle.
One way to set the sort order of a DataObject is to set the $default_sort variable:
class CustomDataObject extends DataObject {
private static $db = array(
'Active' => 'Boolean',
'SortOrder' => 'Int'
);
private static $has_one = array(
'Page' => 'CustomPage'
);
private static $default_sort = 'SortOrder ASC';
}
Make sure you call ?flush=all after doing this to clear the site cache.
Also, if the custom DataObject is being maintained through a GridField we can use module to control the sort order through drag and drop. Here is a StackOverflow answer detailing how to use one of these modules:
Silverstripe DataObject - drag and drop ordering
As 3dgoo stated $default_sortis your friend. When your column sort (or SortOrder) isn't unique you can always add another column to sort duplicates, so something like
private static $default_sort = 'sort, ID ASC';
should work in your case.

How to use KNP Translatable in a form type

I'm using KNP Translatable and I have the following data structure:
User (id, name, email, password...)
Role (id, name #translatable)
User Role is a many to many relation.
I have the form type defined as this:
->add('roles', 'entity', [
'class' => 'SocialCarBackendBundle:Role',
'property' => 'name',
'multiple' => true,
'expanded' => true
])
And I implemented the __call method in the role entity:
public function __call($method, $arguments)
{
try {
return $this->proxyCurrentLocaleTranslation($method, $arguments);
} catch (\Symfony\Component\Debug\Exception\ContextErrorException $e) {
return $this->proxyCurrentLocaleTranslation('get' . ucfirst($method), $arguments);
}
}
Now, in the twig template I can call the name property of the roles without problems and it renders it correctly.
But when trying to render the form I get this error:
Neither the property "name" nor one of the methods "getName()",
"name()", "isName()", "hasName()", "__get()" exist and have public
access in class "SocialCar\BackendBundle\Entity\Role".
Is there any workaround for this? Thanks a lot
symfony's propertyaccessor component has not magic calls enabled for EntityType property
you can see vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php to prove that.
so you have three ways(in order of complexity):
do getter and setters that call proxyCurrentLocaleTranslation, imho there are nothing bad using less magic things:)
use a more complex property like this
'property' => 'translations[' . $options['locale'] . '].name',
where $options['locale'] is the locale injected inside the form as an option
you can create a different EntityType class that extends your custom DoctrineType class that initializes PropertyAccessor to support magic calls
for more info about property accessor:
http://symfony.com/doc/current/components/property_access/introduction.html
and about the second way:
https://github.com/KnpLabs/DoctrineBehaviors/issues/67

Resources