I am new to AEM, consider my js would return itemList (for ex: var itemList = list.getItems();).
The skeleton of each Item would be:
interface Item {
/**
* Get the item's title.
*
* #return the title
*/
#Nonnull
String getTitle();
/**
* Get the item's localized title.
*
* #param locale the locale for localization
*
* #return the title
*/
#Nonnull
String getTitle(Locale locale);
/**
* Get the item's value.
*
* #return the value
*/
#Nonnull
String getValue();
}
How can I get the locale based title (i.e calling getTitle(locale)) in place of ${list.title} mentioned in the below HTML code of select tag (I need both title(locale) and value in the itemlist):
option value="${list.value}">${list.title}
From Sightly, you cannot access methods with parameters.
However, in the Use-API, the same global variables are available as in the Sightly execution context, and you can also pass parameters to the initialization of the Use-API.
In Java code, you can for instance access the page language with something like that:
PageManager pageManager = request.getResourceResolver().adaptTo(PageManager.class);
Page currentPage = pageManager.getContainingPage(request.getResource());
Locale locale = currentPage.getLanguage(false);
Else, to pass parameters to the Java Use-API, this is how it goes:
<div data-sly-use.foo="${'Foo' # locale='es'}">
<h1>${foo.titleLocalized}</h1>
</div>
And the corresponding Java:
public class Foo extends WCMUsePojo {
private String locale;
#Override
public void activate() throws Exception {
String locale = get("locale", String.class);
}
public String titleLocalized() {
...
}
}
Related
I would like to return a different result for a computed field when viewing the index view than when viewing the detail view of a resource.
Basically something like viewIs() below:
Text::make('Preview', function () {
if($this->viewIs('index'){
return \small_preview($this->image);
}
return \large_preview($this->image);
})->asHtml(),
You can check the class of the request:
Text::make('Preview', function () use ($request) {
if ($request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Otherwise, you can create your own viewIs function:
// app/Nova/Resource.php
/**
* Check the current view.
*
* #param string $view
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #retrun bool
*/
public function viewIs($view, $request)
{
$class = '\Laravel\Nova\Http\Requests\\Resource'.ucfirst($view).'Request';
return $request instanceof $class;
}
Then you can do it like this:
Text::make('Preview', function () use ($request) {
if ($this->viewIs('detail', $request) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Unfortunately, #Chin's answer did not work for me as for Edit view the request class is just a basic Laravel\Nova\Http\Request class.
My workaround to check if this is an index view is as follows:
/**
* Check if the current view is an index view.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #return bool
*/
public function isIndex($request)
{
return $request->resourceId === null;
}
The NovaRequest class will soon be able to help, as the isResourceIndexRequest and isResourceDetailRequest are already in master.
As the Nova repo is private I will keep you posted, when it will be usable.
In the meantime I am falling back to helper methods on the Nova Resource class (app/Nova/Resource.php):
namespace App\Nova;
use Laravel\Nova\Http\Requests\ResourceDetailRequest;
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
use Laravel\Nova\Resource as NovaResource;
use Laravel\Nova\Http\Requests\NovaRequest;
abstract class Resource extends NovaResource
{
// [...]
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest($request)
{
return $request instanceof ResourceIndexRequest;
}
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest($request)
{
return $request instanceof ResourceDetailRequest;
}
}
Usage:
public function fields(Request $request)
{
$fields = [
// [...]
];
if ($this->isResourceDetailRequest($request)) {
if ($this->isResourceDetailRequest($request)) {
$fields = array_merge($fields, [
// [...]
]);
}
}
return $fields;
}
I added this little helper class
namespace App\Helpers;
class CurrentResourceAction {
public static function isIndex($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceIndexRequest;
}
public static function isDetail($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest;
}
public static function isCreate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'create';
}
public static function isUpdate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'update';
}
}
you can call it anywhere you need to
A bit late but hey! You can check against the NovaRequest properties editing and editMode ('create', 'update', 'attach' etc.)
// Determine if you are creating a model.
$request->editMode == 'create';
Or as they say, "Read the source Luke" and see how they determine it. See the Laravel\Nova\Http\Requests\NovaRequest, it contains similar checks.
namespace Laravel\Nova\Http\Requests;
class NovaRequest extends FormRequest
{
/**
* Determine if this request is via a many to many relationship.
*
* #return bool
*/
public function viaManyToMany();
/**
* Determine if this request is an attach or create request.
*
* #return bool
*/
public function isCreateOrAttachRequest();
/**
* Determine if this request is an update or update-attached request.
*
* #return bool
*/
public function isUpdateOrUpdateAttachedRequest();
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest();
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest();
/**
* Determine if this request is an action request.
*
* #return bool
*/
public function isActionRequest();
}
Would've been nice if you can type hint the NovaRequest instead of the regular one in the Nova resource fields() method but it is not allowed due to the parent resource being extended.
You can create two separate fields for index & details page.
// ----- For Index page
Text::make('Preview', function () {
return \small_preview($this->image);
})
->onlyOnIndex()
->asHtml(),
// ----- For Detail page
Text::make('Preview', function () {
return \large_preview($this->image);
})
->onlyOnDetail()
->asHtml(),
I'd like to assign a style class to a column in Grid. The Column class does not provide an addStyleName method that other Vaadin components do. Is there a way to do it?
You can only set an CellStyleGenerator or RowStyleGenerator for the grid. To set a class for a column, you have to do this:
grid.setCellStyleGenerator(new CellStyleGenerator() {
#Override
public String getStyle(CellReference cell) {
if ("myProperty".equals(cell.getPropertyId()))
return "my-style";
else
return null;
}
});
There can be only single CellStyleGenerator for single Grid. I often have complex code that configures the grid, and I configure it column by column. I use this utility class, that enables me to do so (requires Java8):
/**
* A {#link CellStyleGenerator}, that enables you to set <code>CellStyleGenerator</code>
* independently for each column. It also has a shorthand method to set a fixed style
* class for a column, which Vaadin currently does not allow to (as of Vaadin 7.6).
*
* For more information, see http://stackoverflow.com/a/36398300/952135
* #author http://stackoverflow.com/users/952135
*/
public class EasyCellStyleGenerator implements CellStyleGenerator {
private Map<Object, List<CellStyleGenerator>> generators;
#Override
public String getStyle(CellReference cellReference) {
if (generators != null) {
List<CellStyleGenerator> gens = generators.get(cellReference.getPropertyId());
if (gens != null)
return gens.stream()
.map(gen -> gen.getStyle(cellReference))
.filter(s -> s != null)
.collect(Collectors.joining(" "));
}
return null;
}
/**
* Adds a generator for a column. Allows generating different style for each cell,
* but is called only for the given column.
*/
public void addColumnCellStyleGenerator(Object propertyId,
CellStyleGenerator generator) {
if (generators == null) // lazy init of map
generators = new HashMap<>();
generators.computeIfAbsent(propertyId, k->new ArrayList<>()).add(generator);
}
/**
* Sets a fixed style class(es), that will be used for all cells of this column.
*/
public void addColumnFixedStyle(Object propertyId, String styleClasses) {
addColumnCellStyleGenerator(propertyId, cellReference -> styleClasses);
}
}
Working with PHPUnit and Doctrine I frequently end up writing very large methods for mocking Doctrines ClassMetadata, although in my opinion it does not need to be mocked, because it can be seen as stable. Still I need to mock the EntityManager because I don't want Doctrine to connect to a database.
So here's my question: How do I get my ClassMetadata via the EntityManager mock without needing a database connection? For all eventual database calls the EntityManager still needs to be a mock, I just don't want to write all my metadata down again.
I'm using the DoctrineModule for Zend 2 so it would be useful to be able to use my configuration to get the Metadata object, but I assume it's also ok to read the required sections manually.
Example:
public function testGetUniqueFields()
{
$this->prepareGetUniqueFields(); // about 50 lines of mocking ClassMetadata
$entity = 'UniqueWithoutAssociation';
$unique = $this->handler->getUniqueFields($entity);
$expected = ["uniqueColumn"];
$this->assertEquals($expected, $unique,
'getUniqueFields does not return the unique fields');
}
And the code of the actual class:
public function getUniqueFields($class)
{
$unique = array();
$metadata = $this->getClassMetadata($class);
$fields = $metadata->getFieldNames();
foreach ($fields as $field) {
if($metadata->isUniqueField($field) && !$metadata->isIdentifier($field)) {
$unique[] = $field;
}
}
return $unique;
}
The test works like expected, but every time I test another method or another behavior of the method, I need to prepare the mocks again or combine past definitions. Plus, the 50 lines I need for this code are the least I have in this test. Most of the test class is all about the ClassMetadata mock. It is a time consuming and - if you see ClassMetadata as a stable component - unnecessary work.
After spending many hours starring at the Doctrine source code, I found a solution.
Once again, this solution is only if you are working with Doctrines ClassMetadata object so often that it becomes unclean to mock every method call. In every other case you should still create a mock of ClassMetadata.
Still, since the composers minimum stability setting was set to stable, such components can be seen as stable so there is no absolute need to create a mock object.
ClassMetadata depends on several other Doctrine classes, which are all injected through the ubiquitous EntityManager:
Doctrine\ORM\Configuration to get the entity path
Doctrine\Common\Annotations\AnnotationReader and Doctrine\ORM\Mapping\Driver\AnnotationDriver injected through the Configuration object
Doctrine\DBAL\Connection to get the database platform in order to know about the identifier strategy. This object should be mocked so no database calls are possible
Doctrine\DBAL\Platforms\AbstractPlatform as mentioned
Doctrine\Common\EventManager to trigger some events
For single test methods or simple method calls I created a method returning an EntityManager mock object that is capable of returning a valid ClassMetadata object:
/**
* #return EntityManager|\PHPUnit_Framework_MockObject_MockObject
*/
public function getEmMock()
{
$dir = __DIR__."/Asset/Entity/";
$config = Setup::createAnnotationMetadataConfiguration(array($dir), true);
$eventManager = new \Doctrine\Common\EventManager();
$platform = new PostgreSqlPlatform();
$metadataFactory = new ClassMetadataFactory();
$config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
$connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
->disableOriginalConstructor()
->getMock();
$connectionMock->expects($this->any())
->method('getDatabasePlatform')
->will($this->returnValue($platform));
/** #var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
$emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$metadataFactory->setEntityManager($emMock);
$emMock->expects($this->any())
->method('getConfiguration')
->will($this->returnValue($config));
$emMock->expects($this->any())
->method('getConnection')
->will($this->returnValue($connectionMock));
$emMock->expects($this->any())
->method('getEventManager')
->will($this->returnValue($eventManager));
$emMock->expects($this->any())
->method('getClassMetadata')
->will($this->returnCallback(function($class) use ($metadataFactory){
return $metadataFactory->getMetadataFor($class);
}));
return $emMock;
}
Here you could even manipulate all objects by calling their getters created for the EntityManager mock. But that wouldn't be exactly clean and the method remains inflexible in some cases. Still a simple solution and you could e.g. add some parameters and put the method in a trait to reuse it.
For further needs, I created an abstract class that offers a maximum of flexibility and allows you to mock everything else or create some components in a whole other way.
It needs two configurations: The entity path and the platform object. You can manipulate or replace any object by setting it in the setUp method and then get the required EntityManager mock with getEmMock().
A little bit larger, but here it is:
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;
/**
* Class AbstractTestWithMetadata
* #author Marius Teller
*/
abstract class AbstractTestWithMetadata extends \PHPUnit_Framework_TestCase
{
const EXCEPTION_NO_ENTITY_PATHS_SET = "At least one entity path must be set";
const EXCEPTION_NO_PLATFORM_SET = "An instance of Doctrine\\DBAL\\Platforms\\AbstractPlatform must be set";
/**
* #var array
*/
protected $entityPaths = [];
/**
* #var AbstractPlatform
*/
protected $platform;
/**
* #var EntityManager
*/
protected $emMock;
/**
* #var Connection
*/
protected $connectionMock;
/**
* #var Configuration
*/
protected $configuration;
/**
* #var EventManager
*/
protected $eventManager;
/**
* #var ClassMetadataFactory
*/
protected $classMetadataFactory;
/**
* #return array
* #throws \Exception
*/
public function getEntityPaths()
{
if($this->entityPaths === []) {
throw new \Exception(self::EXCEPTION_NO_ENTITY_PATHS_SET);
}
return $this->entityPaths;
}
/**
* #param array $entityPaths
*/
public function setEntityPaths(array $entityPaths)
{
$this->entityPaths = $entityPaths;
}
/**
* add an entity path
* #param string $path
*/
public function addEntityPath($path)
{
$this->entityPaths[] = $path;
}
/**
* #return AbstractPlatform
* #throws \Exception
*/
public function getPlatform()
{
if(!isset($this->platform)) {
throw new \Exception(self::EXCEPTION_NO_PLATFORM_SET);
}
return $this->platform;
}
/**
* #param AbstractPlatform $platform
*/
public function setPlatform(AbstractPlatform $platform)
{
$this->platform = $platform;
}
/**
* #return EntityManager
*/
public function getEmMock()
{
if(!isset($this->emMock)) {
/** #var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
$emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$config = $this->getConfiguration();
$connectionMock = $this->getConnectionMock();
$eventManager = $this->getEventManager();
$classMetadataFactory = $this->getClassMetadataFactory();
$classMetadataFactory->setEntityManager($emMock);
$emMock->expects($this->any())
->method('getConfiguration')
->will($this->returnValue($config));
$emMock->expects($this->any())
->method('getConnection')
->will($this->returnValue($connectionMock));
$emMock->expects($this->any())
->method('getEventManager')
->will($this->returnValue($eventManager));
$emMock->expects($this->any())
->method('getClassMetadata')
->will($this->returnCallback(function($class) use ($classMetadataFactory){
return $classMetadataFactory->getMetadataFor($class);
}));
$this->setEmMock($emMock);
}
return $this->emMock;
}
/**
* #param EntityManager $emMock
*/
public function setEmMock($emMock)
{
$this->emMock = $emMock;
}
/**
* #return Connection
*/
public function getConnectionMock()
{
if(!isset($this->connectionMock)) {
$platform = $this->getPlatform();
/** #var Connection|\PHPUnit_Framework_MockObject_MockObject $connectionMock */
$connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
->disableOriginalConstructor()
->getMock();
$connectionMock->expects($this->any())
->method('getDatabasePlatform')
->will($this->returnValue($platform));
$this->setConnectionMock($connectionMock);
}
return $this->connectionMock;
}
/**
* #param Connection $connectionMock
*/
public function setConnectionMock($connectionMock)
{
$this->connectionMock = $connectionMock;
}
/**
* #return Configuration
*/
public function getConfiguration()
{
if(!isset($this->configuration)) {
$config = Setup::createAnnotationMetadataConfiguration($this->getEntityPaths(), true);
$config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
$this->setConfiguration($config);
}
return $this->configuration;
}
/**
* #param Configuration $configuration
*/
public function setConfiguration(Configuration $configuration)
{
$this->configuration = $configuration;
}
/**
* #return EventManager
*/
public function getEventManager()
{
if(!isset($this->eventManager)) {
$this->setEventManager(new EventManager());
}
return $this->eventManager;
}
/**
* #param EventManager $eventManager
*/
public function setEventManager($eventManager)
{
$this->eventManager = $eventManager;
}
/**
* #return ClassMetadataFactory
*/
public function getClassMetadataFactory()
{
if(!isset($this->classMetadataFactory)) {
$this->setClassMetadataFactory(new ClassMetadataFactory());
}
return $this->classMetadataFactory;
}
/**
* #param ClassMetadataFactory $classMetadataFactory
*/
public function setClassMetadataFactory(ClassMetadataFactory $classMetadataFactory)
{
$this->classMetadataFactory = $classMetadataFactory;
}
}
One more hint: You might have problems with annotations of other classes, e.g. Zend\Form\Annotation\Validator. Such annotations will throw an exception in Doctrines parser because this parser does not use auto loading and only checks for already loaded classes. So if you still want to use them, you just have to include them manually before parsing the classes annotations.
Problem
Hello, I am using Symfony CMF 1.2, liip/imagine-bundle 1.3, symfony-cmf/media-bundle 1.2. I want to add 2 additional image fields to my block that extends ImagineBlock because for every image I upload there will be a mobile and tablet version of the image which is not a simple resize, the aspect ratio or whatnot is not similar. I cannot just crop/resize without affecting the quality of the image.
Attempts
My block
namespace xx\BlockBundle\Document;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
use Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock;
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Symfony\Cmf\Bundle\MediaBundle\ImageInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Class ClickableBlock
* #package xx\BlockBundle\Document
* #PHPCR\Document(referenceable=true)
*/
class ClickableBlock extends ImagineBlock
{
/**
* #PHPCR\Child(nodeName="image-mobile", cascade={"persist"})
* #var Image
*/
protected $imageMobile;
/**
* #PHPCR\Child(nodeName="image-tablet", cascade={"persist"})
* #var Image
*/
protected $imageTablet;
public function setIsPublishable($publishable)
{
$this->setPublishable($publishable);
}
/**
* #return Image
*/
public function getImageMobile()
{
return $this->imageMobile;
}
/**
* #return Image
*/
public function getImageTablet()
{
return $this->imageTablet;
}
/**
* Set the imageMobile for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageMobile to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageMobile($image = null)
{
return $this->processImage($image, 'image-mobile', $this->imageMobile);
}
/**
* Set the imageTablet for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageTablet to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageTablet($image = null)
{
return $this->processImage($image, 'image-tablet', $this->imageTablet);
}
/**
* #param ImageInterface|UploadedFile|null $image
* #param string $imageName
* #param Image $imageRef
* #return $this
*/
protected function processImage($image, $imageName, $imageRef)
{
if (!$image) {
return $this;
}
if (!$image instanceof ImageInterface && !$image instanceof UploadedFile) {
$type = is_object($image) ? get_class($image) : gettype($image);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($imageRef) {
// existing imageTablet, only update content
$imageRef->copyContentFromFile($image);
} elseif ($image instanceof ImageInterface) {
$image->setName($imageName); // ensure document has right name
$imageRef = $image;
} else {
$imageRef = new Image();
$imageRef->copyContentFromFile($image);
}
return $this;
}
}
Admin:
namespace xx\BlockBundle\Admin;
use xx\BlockBundle\Document\ClickableBlock;
use xx\MainBundle\Form\Common\FormMapper as CommonFormMapper;
use Cocur\Slugify\Slugify;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Cmf\Bundle\BlockBundle\Admin\Imagine\ImagineBlockAdmin;
class ClickableBlockAdmin extends ImagineBlockAdmin
{
/**
* {#inheritdoc}
*/
public function toString($object)
{
return $object instanceof ClickableBlock && $object->getLabel()
? $object->getLabel()
: parent::toString($object);
}
/**
* {#inheritdoc}
*/
public function prePersist($document)
{
parent::prePersist($document);
$this->InitialiseDocument($document);
}
/**
* #param $document
*/
private function InitialiseDocument(&$document)
{
$documentManager = $this->getModelManager();
$parentDocument = $documentManager->find(null, '/cms/xx/block');
$document->setParentDocument($parentDocument);
$slugifier = new Slugify();
$document->setName($slugifier->slugify($document->getLabel()));
}
/**
* {#inheritdoc}
*/
public function preUpdate($document)
{
parent::preUpdate($document);
$this->InitialiseDocument($document);
}
/**
* {#inheritdoc}
*/
protected function configureFormFields(FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
if (null === $this->getParentFieldDescription()) {
$imageRequired = ($this->getSubject() && $this->getSubject()->getParentDocument()) ? false : true;
$formMapper
->with('form.group_general')
->remove('parentDocument')
->remove('filter')
->add('parentDocument', 'hidden', ['required' => false, 'data' => 'filler'])
->add('name', 'hidden', ['required' => false, 'data' => 'filler'])
->add('imageMobile', 'cmf_media_image', array('required' => $imageRequired))
->add('imageTablet', 'cmf_media_image', array('required' => $imageRequired))
->end();
// Append common fields to FormMapper
$commonFormMapper = new CommonFormMapper($formMapper);
$formMapper = $commonFormMapper->getPublishingFields();
}
}
}
Note I am unable to inject service container to this class (via constructor/method), that is why am using hardcoded node path and instantiated Slugify class instead of using it's service for now. I am all ears for a solution to this also. Ref -
xx.main.admin.pageadmin.container:
class: xx\MainBundle\Admin\PageAdmin
calls:
- [setContainer,[ #service_container ]]
# arguments: ["#service_container"]
The annotations on the image fields are based on the following config I found in
\vendor\symfony-cmf\block-bundle\Resources\config\doctrine-phpcr\ImagineBlock.phpcr.xml:
<doctrine-mapping
xmlns="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping
https://github.com/doctrine/phpcr-odm/raw/master/doctrine-phpcr-odm-mapping.xsd"
>
<document
name="Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock"
referenceable="true"
translator="attribute"
>
<node name="node"/>
<locale name="locale"/>
<field name="label" type="string" translated="true" nullable="true"/>
<field name="linkUrl" type="string" translated="true" nullable="true"/>
<field name="filter" type="string" nullable="true"/>
<child name="image" node-name="image">
<cascade>
<cascade-persist/>
</cascade>
</child>
</document>
</doctrine-mapping>
Result
While the default "image" field persists normally, the other two added image fields are not taken into consideration since when I debug on prePersist I see that both fields are null while image field contains its uploaded file.
I tried adding a normal text field which saved and displayed normally on my page.
I use YAML in my project, so I am not sure how exactly the given XML translates, if ever it is the correct mapping to define.
Please help. :)
A colleague found the issue which was the following:
protected function processImage($image, $imageName, $imageRef)
should be
protected function processImage($image, $imageName, &$imageRef)
$imageRef was not passed by reference making it always null. Silly me. Let's hope this code at least helps other people. :)
For the admin question: phpcr-odm admins have a rootPath for exactly the purpose of what you are doing. you could add to your service definition like this:
<call method="setRootPath">
<argument>%cmf_content.persistence.phpcr.content_basepath%</argument>
</call>
and then you do $this->getRootPath()
lets say I have an ActivePivot cube with facts containing just Value, and Currency.
lets say my cube has Currency as a regular dimension.
We fill the cube with facts that have many currencies.
We have a forex service that takes the currency and reference currency to get a rate.
Now, Value.SUM doesn't make any sense, we are adding up values with different currencies, so we want to have a post processor that can convert all values to a reference currency, say, USD, then sum them, so we write a post processor that extends ADynamicAggregationPostProcessor, specify Currency as a leaf level dimension, and use the forex service to do the conversion, and we are happy.
But, lets say we don't want to convert just to USD, we want to convert to 10 different currencies and see the results next to each other on the screen.
So we create an Analysis dimension, say ReferenceCurrency, with 10 members.
My question is: how can I alter the above post processor to handle the Analysis dimension? The plain vanilla ADynamicAggregationPostProcessor does not handle Analysis dimensions, only the default member is visible to this post processor. Other post processors that handle Analysis dimensions, like DefaultAggregatePostProcessor do not have a means for specifying leaf levels, so I cannot get the aggregates by Currency, and so cannot do the forex conversion. How can I have my cake and eat it too?
It looks like you want to use two advanced features of ActivePivot at the same time (Analysis dimensions to expose several outcomes of the same aggregate, and dynamic aggregation to aggregate amounts expressed in different currencies).
Separately each one is fairly easy to setup through configuration and a few lines of code to inject. But to interleave both you will need to understand the internals of post processor evaluation, and inject business logic at the right places.
Here is an example based on ActivePivot 4.3.3. It has been written in the open-source Sandbox Application so that you can run it quickly before adapting it to your own project.
First we need a simple analysis dimension to hold the possible reference currencies:
package com.quartetfs.pivot.sandbox.postprocessor.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import com.quartetfs.biz.pivot.cube.hierarchy.axis.impl.AAnalysisDimension;
import com.quartetfs.fwk.QuartetExtendedPluginValue;
/**
*
* An analysis dimension bearing the
* list of possible reference currencies.
*
* #author Quartet FS
*
*/
#QuartetExtendedPluginValue(interfaceName = "com.quartetfs.biz.pivot.cube.hierarchy.IDimension", key = ReferenceCurrencyDimension.TYPE)
public class ReferenceCurrencyDimension extends AAnalysisDimension {
/** serialVersionUID */
private static final long serialVersionUID = 42706811331081328L;
/** Default reference currency */
public static final String DEFAULT_CURRENCY = "EUR";
/** Static list of non-default possible reference currencies */
public static final List<Object[]> CURRENCIES;
static {
List<Object[]> currencies = new ArrayList<Object[]>();
currencies.add(new Object[] {"USD"});
currencies.add(new Object[] {"GBP"});
currencies.add(new Object[] {"JPY"});
CURRENCIES = Collections.unmodifiableList(currencies);
}
/** Plugin type */
public static final String TYPE = "REF_CCY";
/** Constructor */
public ReferenceCurrencyDimension(String name, int ordinal, Properties properties, Set<String> measureGroups) {
super(name, ordinal, properties, measureGroups);
}
#Override
public Object getDefaultDiscriminator(int levelOrdinal) { return DEFAULT_CURRENCY; }
#Override
public Collection<Object[]> buildDiscriminatorPaths() { return CURRENCIES; }
#Override
public int getLevelsCount() { return 1; }
#Override
public String getLevelName(int levelOrdinal) {
return levelOrdinal == 0 ? "Currency" : super.getLevelName(levelOrdinal);
}
#Override
public String getType() { return TYPE; }
}
Then the post processor itself, a customized dynamic aggregation post processor modified to handle the analysis dimension and output the same aggregate multiple times, one time per reference currency.
package com.quartetfs.pivot.sandbox.postprocessor.impl;
import java.util.List;
import java.util.Properties;
import com.quartetfs.biz.pivot.IActivePivot;
import com.quartetfs.biz.pivot.ILocation;
import com.quartetfs.biz.pivot.ILocationPattern;
import com.quartetfs.biz.pivot.aggfun.IAggregationFunction;
import com.quartetfs.biz.pivot.cellset.ICellSet;
import com.quartetfs.biz.pivot.cube.hierarchy.IDimension;
import com.quartetfs.biz.pivot.cube.hierarchy.axis.IAxisMember;
import com.quartetfs.biz.pivot.impl.Location;
import com.quartetfs.biz.pivot.postprocessing.impl.ADynamicAggregationPostProcessor;
import com.quartetfs.biz.pivot.postprocessing.impl.ADynamicAggregationProcedure;
import com.quartetfs.biz.pivot.query.IQueryCache;
import com.quartetfs.biz.pivot.query.aggregates.IAggregatesRetriever;
import com.quartetfs.biz.pivot.query.aggregates.RetrievalException;
import com.quartetfs.fwk.QuartetException;
import com.quartetfs.fwk.QuartetExtendedPluginValue;
import com.quartetfs.pivot.sandbox.service.impl.ForexService;
import com.quartetfs.tech.type.IDataType;
import com.quartetfs.tech.type.impl.DoubleDataType;
/**
* Forex post processor with two features:
* <ul>
* <li>Dynamically aggregates amounts in their native currencies into reference currency
* <li>Applies several reference currencies, exploded along an analysis dimension.
* </ul>
*
* #author Quartet FS
*/
#QuartetExtendedPluginValue(interfaceName = "com.quartetfs.biz.pivot.postprocessing.IPostProcessor", key = ForexPostProcessor.TYPE)
public class ForexPostProcessor extends ADynamicAggregationPostProcessor<Double> {
/** serialVersionUID */
private static final long serialVersionUID = 15874126988574L;
/** post processor plugin type */
public final static String TYPE = "FOREX";
/** Post processor return type */
private static final IDataType<Double> DATA_TYPE = new DoubleDataType();
/** Ordinal of the native currency dimension */
protected int nativeCurrencyDimensionOrdinal;
/** Ordinal of the native currency level */
protected int nativeCurrencyLevelOrdinal;
/** Ordinal of the reference currencies dimension */
protected int referenceCurrenciesOrdinal;
/** forex service*/
private ForexService forexService;
/** constructor */
public ForexPostProcessor(String name, IActivePivot pivot) {
super(name, pivot);
}
/** Don't forget to inject the Forex service into the post processor */
public void setForexService(ForexService forexService) {
this.forexService = forexService;
}
/** post processor initialization */
#Override
public void init(Properties properties) throws QuartetException {
super.init(properties);
nativeCurrencyDimensionOrdinal = leafLevelsOrdinals.get(0)[0];
nativeCurrencyLevelOrdinal = leafLevelsOrdinals.get(0)[1];
IDimension referenceCurrenciesDimension = getDimension("ReferenceCurrencies");
referenceCurrenciesOrdinal = referenceCurrenciesDimension.getOrdinal();
}
/**
* Handling of the analysis dimension:<br>
* Before retrieving leaves, wildcard the reference currencies dimension.
*/
protected ICellSet retrieveLeaves(ILocation location, IAggregatesRetriever retriever) throws RetrievalException {
ILocation baseLocation = location;
if(location.getLevelDepth(referenceCurrenciesOrdinal-1) > 0) {
Object[][] array = location.arrayCopy();
array[referenceCurrenciesOrdinal-1][0] = null; // wildcard
baseLocation = new Location(array);
}
return super.retrieveLeaves(baseLocation, retriever);
}
/**
* Perform the evaluation of the post processor on a leaf (as defined in the properties).
* Here the leaf level is the UnderlierCurrency level in the Underlyings dimension .
*/
#Override
protected Double doLeafEvaluation(ILocation leafLocation, Object[] underlyingMeasures) throws QuartetException {
// Extract the native and reference currencies from the evaluated location
String currency = (String) leafLocation.getCoordinate(nativeCurrencyDimensionOrdinal-1, nativeCurrencyLevelOrdinal);
String refCurrency = (String) leafLocation.getCoordinate(referenceCurrenciesOrdinal-1, 0);
// Retrieve the measure in the native currency
double nativeAmount = (Double) underlyingMeasures[0];
// If currency is reference currency or measureNative is equal to 0.0 no need to convert
if ((currency.equals(refCurrency)) || (nativeAmount == .0) ) return nativeAmount;
// Retrieve the rate and rely on the IQueryCache
// in order to retrieve the same rate for the same currency for our query
IQueryCache queryCache = pivot.getContext().get(IQueryCache.class);
Double rate = (Double) queryCache.get(currency + "_" + refCurrency);
if(rate == null) {
Double rateRetrieved = forexService.retrieveQuotation(currency, refCurrency);
Double rateCached = (Double) queryCache.putIfAbsent(currency + "_" + refCurrency, rateRetrieved);
rate = rateCached == null ? rateRetrieved : rateCached;
}
// Compute equivalent in reference currency
return rate == null ? nativeAmount : nativeAmount * rate;
}
#Override
protected IDataType<Double> getDataType() { return DATA_TYPE; }
/** #return the type of this post processor, within the post processor extended plugin. */
#Override
public String getType() { return TYPE; }
/**
* #return our own custom dynamic aggregation procedure,
* so that we can inject our business logic.
*/
protected DynamicAggregationProcedure createProcedure(ICellSet cellSet, IAggregationFunction aggregationFunction, ILocationPattern pattern) {
return new DynamicAggregationProcedure(cellSet, aggregationFunction, pattern);
}
/**
* Custom dynamic aggregation procedure.<br>
* When the procedure is executed over a leaf location,
* we produce several aggregates instead of only one:
* one aggregate for each of the visible reference currencies.
*/
protected class DynamicAggregationProcedure extends ADynamicAggregationProcedure<Double> {
protected DynamicAggregationProcedure(ICellSet cellSet, IAggregationFunction aggregationFunction, ILocationPattern pattern) {
super(ForexPostProcessor.this, aggregationFunction, cellSet, pattern);
}
/**
* Execute the procedure over one row of the leaf cell set.
* We compute one aggregate for each of the reference currencies.
*/
#Override
public boolean execute(ILocation location, int rowId, Object[] measures) {
if(location.getLevelDepth(referenceCurrenciesOrdinal-1) > 0) {
// Lookup the visible reference currencies
IDimension referenceCurrenciesDimension = pivot.getDimensions().get(referenceCurrenciesOrdinal);
List<IAxisMember> referenceCurrencies = (List<IAxisMember>) referenceCurrenciesDimension.retrieveMembers(0);
for(IAxisMember member : referenceCurrencies) {
Object[][] array = location.arrayCopy();
array[referenceCurrenciesOrdinal-1][0] = member.getDiscriminator();
ILocation loc = new Location(array);
super.execute(loc, rowId, measures);
}
return true;
} else {
return super.execute(location, rowId, measures);
}
}
#Override
protected Double doLeafEvaluation(ILocation location, Object[] measures) throws QuartetException {
return ForexPostProcessor.this.doLeafEvaluation(location, measures);
}
}
}
In the description of your cube, the analysis dimension and the post processor would be exposed like this:
...
<dimension name="ReferenceCurrencies" pluginKey="REF_CCY" />
...
<measure name="cross" isIntrospectionMeasure="false">
<postProcessor pluginKey="FOREX">
<properties>
<entry key="id" value="pv.SUM" />
<entry key="underlyingMeasures" value="pv.SUM" />
<entry key="leafLevels" value="UnderlierCurrency#Underlyings" />
</properties>
</postProcessor>
</measure>
...
Analysis Dimensions brings so much complexity they should be considered apart of the others features of your cube. One way to handle your issue is then:
Add a first measure expanding properly along the analysis dimension. In your case, it will simply copy the underlying measure along ReferenceCurrency, optionally doing the FX conversion. This measure may be used as underlying of several measures.
Add a second measure, based on usual dynamic aggregation. This second implementation is very simple as it does not know there is an analysis dimension.