How to add custom condition to Sonata global search feature - symfony

I would like to add a custom condition to the queries which are generated by Sonata Search feature. The problem is that i have 'status' column which should be set as "active". On the List View i do not have any problem because I am able to set:
protected $datagridValues = array (
'status' => array ('type' => 1, 'value' => Status::ACTIVE)
);
and then all queries check if the status field is set properly.
But the problem is with global search. I can override SearchHandler and force desired behavior, but i can't change any files from vendor/ directory, so i have two questions.
How can i inject my own SearchHandler, which configuration file i need to change and how
Maybe there is a simpler way to develope needed solution?

SOLUTION:
I have figure out how can i inject my own SearchHandler. The following code is used for that:
1. Just edit your services.yml file and put something like that:
cmsbundle.search.handler:
class: XXX\CmsBundle\Search\SearchHandler
arguments:
- #sonata.admin.pool
sonata.admin.block.search_result:
class: XXX\CmsBundle\Search\AdminSearchBlockService
tags:
- { name: sonata.block }
arguments:
- sonata.admin.block.search_result
- #templating
- #sonata.admin.pool
- #cmsbundle.search.handler
Create the file "XXX\CmsBundle\Search\AdminSearchBlockService" and change SearchHandler instance to yours own
Create the file "XXX\CmsBundle\Search\SearchHandler" and change implementation. It can be something like that:
foreach ($datagrid->getFilters() as $name => $filter) {
/** #var $filter FilterInterface */
if ($filter->getOption('global_search', false)) {
if ($filter->getName() !== 'status') {
$filter->setCondition(FilterInterface::CONDITION_OR);
$datagrid->setValue($name, null, $term);
} else {
$filter->setCondition(FilterInterface::CONDITION_AND);
$datagrid->setValue($name, null, 'active');
}
$found = true;
}
}
IMPORTANT
'status' field must be added to configureDatagridFilters method in Admin class.

I thought I'd add my solution to this problem.
My problem was similar, my Admin class would modify the Admin Entities' respective createQuery. This query would add in restrictions so that the user can only view their models, or only view things which are not deleted for example.
The problem is the SearchHandler.php would set ALL filters as
$filter->setCondition(FilterInterface::CONDITION_OR);
This would cause queries to look like:
( myAddedCondition OR filterCondition OR filterCondition OR filterCondition )
What I really wanted instead was:
( myAddedCondition ) AND ( filterCondition OR filterCondition OR filterCondition )
In order to achieve this, I registered a GLOBAL ASTWalker which iomplements the walkWhereClause method.. Then in the Walker I would manually edit the generated SQL to suit my requirements.

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

Silverstripe 3 - Disable field validation for CMS delete operations only

I have a custom field type that contains a small validator. It is used all over the place on the Frontend, so for convenience we also use it in the CMS.
class MyTextField extends TextField {
public function Type() {
return 'text';
}
public function getAttributes() {
return array_merge(
parent::getAttributes(),
array(
'type' => 'text'
)
);
}
public function validate($validator) {
$this->value = trim($this->value);
$maxLength = empty($this->getMaxLength())?50:$this->getMaxLength();
if(strlen($this->value)>$maxLength) {
$validator->validationError(
$this->name,
'Exceeded max length '.$maxLength,
"validation"
);
return false;
}else{
return true;
}
}
}
}
Field works fine. If a create a new instance using it, and that constructor doesn't include a maxlength property, it defaults to 50.
The issue I am having is that a change to a frontend field "MyText" was set to 100, while the version of this field in getCMSFields was not, and thus defaulted to a max length of 50. Obviously validation failure.
Of course this should and has been rectified, but the issue came up as a CMS user could not delete a record. And I don't really disagree with the user in that validation should be ignored when you discarding the data anyway. It would make sense for add/edit, but not really delete.
Anyone know of a way of disabling validation for a delete operation inside getCMSFields? Here is an excerpt of my getCMSFields in case a refactor is required.
function getCMSFields() {
$detailBlock = CompositeField::create(
MyTextField::create('MyTextField','CustomField', '', 255),
);
// Build the fieldlist for the form.
$fields = FieldList::create (
TabSet::create (
"Root",
Tab::create (
"Main Content",
$detailBlock
)
)
);
return $fields;
}
EDIT: I have just discovered that I need a way to bypass getCMSValidator for delete operations too. There are required fields set in here that can trigger a validation error on delete too. It doesn't really make sense that you would need to put a record in a valid state just to delete it.

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

Is it possible to have versioned many_many relations?

I already used versioning on DataObjects when they contain a lot of content, now I'm wondering if it's possible to apply versioning to a many_many relation?
Assuming I have the following:
class Page extends SiteTree
{
private static $many_many = array(
'Images' => 'Image'
);
}
Then the ORM will create a Page_Images table for me to store the relations. In order to have a versioned relation, more tables would be required (eg. Page_Images_Live).
Is there any way to tell the ORM to create versioned relations? When looking at the above example with a Page * – * Images relation, I don't want the Image class to be versioned, but rather the relation. Eg. something like this:
Version Stage:
---
PageA
Images ( ImageA, ImageB, ImageC )
Version Live:
---
PageA
Images ( ImageA, ImageC, ImageD, ImageE )
Is that even possible out of the box?
I've spent a lot of time looking into this and without fundamentally modifying ManyManyList (as it doesn't expose the necessary hooks through the extension system), there isn't many choices.
I am a dessert-first kind of person, how CAN we do it?
My only suggestion to accomplish this feat is essentially a many-to-many bridge object (ie. a separate entity joining Page and Image) via $has_many though it still requires quite a bit of modification.
This is partially discussed on the forum where a solution about subverting the actual relationship by storing the versioned items against the actual object rather than in a joining table. That would work but I think we can still do better than that.
I am personally leaning towards tying the version of the relationship to the Page itself and my partial solution below covers this. Read below the fold for more info trying this as an update to ManyManyList.
Something like this is a start:
class PageImageVersion extends DataObject
{
private static $db = array(
'Version' => 'Int'
);
private static $has_one = array(
'Page' => 'Page',
'Image' => 'Image'
);
}
This contains our 2-way relationship plus we have our version number stored. You will want to specify the getCMSFields function to add the right fields required allowing you to relate it to an existing image or upload a new one. I am avoiding covering this as it should be relatively straight forward compared to the actual version handling part.
Now, we have a has_many on Page like so:
private static $has_many = array(
'Images' => 'PageImageVersion'
);
In my tests, I also added an extension for Image adding the matching $has_many onto it as well like so:
class ImageExtension extends DataExtension
{
private static $has_many = array(
'Pages' => 'PageImageVersion'
);
}
Honestly, not sure if this is necessary beyond adding the Pages
function on the Image side of the relationship. As far as I can see, it won't really matter for this particular usecase.
Unfortunately, because of this way of versioning, we can't use the standard way of calling the Images, we will need to be a bit creative. Something like this:
public function getVersionedImages($Version = null)
{
if ($Version == null)
{
$Version = $this->Version;
}
else if ($Version < 0)
{
$Version = max($this->Version - $Version, 1);
}
return $this->Images()->filter(array('Version' => $Version));
}
When you call getVersionedImages(), it will return all images that have the Version set on it aligning with the version of the current page. Also supports getting previous versions via getVersionedImages(-1) for the last version or even gets images for a specific version of the page by passing any position number.
OK, so far so good. We now need to make sure that every page write we are getting a duplicate list of images for this new version of the page.
With an onAfterWrite function on Page, we can do this:
public function onAfterWrite()
{
$lastVersionImages = $this->getVersionedImages(-1);
foreach ($lastVersionImages as $image)
{
$duplicate = $image->duplicate(false);
$duplicate->Version = $this->Version;
$duplicate->write();
}
}
For those playing at home, this is where things get a bit iffy relating to how restoring previous versions of Page would affect this.
Because we would be editing this in GridField, we will need to do a few things. First is make sure our code can handle the Add New function.
My idea is an onAfterWrite on the PageImageVersion object:
public function onAfterWrite()
{
//Make sure the version is actually saved
if ($this->Version == 0)
{
$this->Version = $this->Page()->Version;
$this->write();
}
}
To get your versioned items displaying in GridField, you would have it set up similar to this:
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridField = new GridField("Images", "Images", $this->getVersionedImages(), $gridFieldConfig);
$fields->addFieldToTab("Root.Images", $gridField);
You might want to link to images directly from the GridField via GridFieldConfig_RelationEditor however this is when things get sour.
Time for the veggies...
One of the big difficulties is GridField, for both linking and unlinking these entities. Using the standard GridFieldDeleteAction will directly update the relationship without the right version.
You will need to extend GridFieldDeleteAction and override the handleAction to write your Page object (to trigger another version), duplicate every version of our versioned image object for the last version while making it skip the one you don't want in the new version.
I'll admit, this last bit is just guesswork by me. From my understanding and debugging, it should work but simply there is a lot of fiddling to get it right.
Your extension of GridFieldDeleteAction then needs to be added to your specific GridField.
This would essentially be your last step away from making this solution work. Once you have the adding, removing, duplicating, version updating part down, it really is a matter of just using getVersionedImages() to get the right images.
Conclusion
Avoid. I get why you want to do this but I really don't see a clean way of being able to handle this without a decent sized update to how many_many relationships are handled in Silverstripe.
But I really want it as a ManyManyList!
The changes I see required for ManyManyList are having a 3-way key (Foreign Key, Local Key, Version Key) and the various methods for adding/removing/fetching etc updated.
If there were hooks in the add and remove functions, you might be able to sneak in the functionality as an extension (via Silverstripe's extension system) and add the needed data to the extra fields that many_many relationships allow.
While I could get this happening by extending ManyManyList directly and then forcing ManyManyList to be replaced with my custom class via Object::useCustomClass, it would be even more of a messy solution.
It is simply too long/complex for me to give a full answer for a pure ManyManyList solution at this stage (though I may get back to this later and give it a shot).
Disclaimer: I am not a Silverstripe Core dev, there may be a neater solution to this entire thing but I simply can't see how.
You can define second relation with "_Live" suffix and update it when the page is published. Note: This solution stores only two versions (live and stage).
Bellow is my implementation which automatically detects whether many-many relation is versioned or not. It then handles publishing and data retrieval. All what is needed is to define one extra many-many relation with "_Live" suffix.
$page->Images() returns items according to the current stage (stage/live).
class Page extends SiteTree
{
private static $many_many = array(
'Images' => 'Image',
'Images_Live' => 'Image'
);
public function publish($fromStage, $toStage, $createNewVersion = false)
{
if ($toStage == 'Live')
{
$this->publishManyToManyComponents();
}
parent::publish($fromStage, $toStage, $createNewVersion);
}
protected function publishManyToManyComponents()
{
foreach (static::getVersionedManyManyComponentNames() as $component_name)
{
$this->publishManyToManyComponent($component_name);
}
}
protected function publishManyToManyComponent($component_name)
{
$stage = $this->getManyManyComponents($component_name);
$live = $this->getManyManyComponents("{$component_name}_Live");
$live_table = $live->getJoinTable();
$live_fk = $live->getForeignKey();
$live_lk = $live->getLocalKey();
$stage_table = $stage->getJoinTable();
$stage_fk = $live->getForeignKey();
$stage_lk = $live->getLocalKey();
// update or add items from stage to live
foreach ($stage as $item)
{
$live->add($item, $stage->getExtraData(null, $item->ID));
}
// delete remaining items from live table
DB::query("DELETE l FROM $live_table AS l LEFT JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk WHERE s.ID IS NULL");
// update new items IDs in live table (IDs are incremental so the new records can only have higher IDs than items in ID => should not cause duplicate IDs)
DB::query("UPDATE $live_table AS l INNER JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk SET l.ID = s.ID WHERE l.ID != s.ID;");
}
public function manyManyComponent($component_name)
{
if (Versioned::current_stage() == 'Live' && static::isVersionedManyManyComponent($component_name))
{
return parent::manyManyComponent("{$component_name}_Live");
}
else
{
return parent::manyManyComponent($component_name);
}
}
protected static function isVersionedManyManyComponent($component_name)
{
$many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
return isset($many_many_components[$component_name]) && isset($many_many_components["{$component_name}_Live"]);
}
protected static function getVersionedManyManyComponentNames()
{
$many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
foreach ($many_many_components as $component_name => $dummy)
{
$is_live = 0;
$stage_component_name = preg_replace('/_Live$/', '', $component_name, -1, $is_live);
if ($is_live > 0 && isset($many_many_components[$stage_component_name]))
{
yield $stage_component_name;
}
}
}
}

how to customise search results of apachesolr (drupal 6)

can somebody help me how to customize the search result of a apache solr search. i was only able to access these variables [comment_count] => [created] => [id] => [name] => [nid] => [title] => [type] => [uid] => [url] => [score] => [body].
how can i access other variable like status, vote .... from the index ( i don't want to access the database for retrieving these values, i want to get it from the index itself)
i need to display no of votes for that specific node in the result snippet
i need to understand
1. how to index votes field
2. how to show the vote, status... in result snippet.
Votes are a poor choice for indexing for a couple of reasons:
Votes can change quickly
When a vote is made, the node is not updated. As such, apachesolr won't know to re-index the node to pick up the change.
If by 'status' you mean the node->status value, then the answer is that it will always be 1. Unpublished nodes are never indexed.
Now, if you want to add something else to the index, you want hook_apachesolr_update_index(&$document, $node) - this hook gets called as each node is being indexed, and you can add fields to $document from $node to get the values into the solr index. However, you want to use the pre-defined field prefixes - look at schema.xml to find the list.
Below is example of code to add fields for sorting, and for output.
/**
* Implementation of hook_apachesolr_update_index()
* Here we're adding custom fields to index, so that they available for sorting. To make this work, it's required to re-index content.
*/
function somemodule_apachesolr_update_index(&$document, $node) {
if ($node->type == 'product') {
$document->addField('sm_default_qty', $node->default_qty);
$document->addField('sm_sell_price', $node->sell_price);
$document->addField('sm_model', $node->model);
foreach ($node->field_images AS $image) {
//$imagecached_filepath = imagecache_create_path('product', $image['filepath']);
$document->addField('sm_field_images', $image['filepath']);
}
}
}
/**
* Implementation of hook_apachesolr_modify_query()
* Here we point what additional fields we need to get from solr
*/
function somemodule_apachesolr_modify_query(&$query, &$params, $caller) {
$params['fl'] .= ',sm_default_qty,sm_field_images,sm_sell_price,sm_model';
}
If you want to totally customize output, you should do following:
1) Copy search-results.tpl.php and search-result.tpl.php from /modules/search to your theme's folder.
2) Use the $result object as needed within search-result.tpl.php
3) Don't forget to clear the theme registry by visiting admin/build/themes
Or as mentioned about, you can override using preprocessor hooks.
Regards, Slava
Another option is to create a view(s) of your liking with input argument nid, then create the following preprocess in your template.php file:
function MYTHEME_preprocess_search_result(&$vars) {
$vars['myView'] = views_embed_view('myView', 'default', $vars['result']['node']->nid);
}
Matching the view name 'myView' with the variable name makes sense to me. Then you can use the variable $myView in your search-results.tpl.php file.
Here's a video by the makers of the Solr Search Integration module with an overview on how to customise what nodes and fields are indexed, and what Solr spits out as a search result...
For Drupal 6:
http://sf2010.drupal.org/conference/sessions/apache-solr-search-mastery.html
And Drupal 7:
http://www.acquia.com/resources/acquia-tv/conference/apache-solr-search-mastery
It all looks very customisable!

Resources