Sorting list of VirtualPages on a field from its Page - silverstripe

I have an AreaPage with $many_many VirtualPages:
class AreaPage extends Page {
/**
* #var array
*/
private static $many_many = [
'RelatedVirtualPages' => 'VirtualPage'
];
// ...
}
The RelatedVirtualPages are copying content from ContentPages:
class ContentPage extends Page {
/**
* #var array
*/
private static $db = [
'Highlighted' => 'Boolean'
];
// ...
}
What's the best way to sort RelatedVirtualPages on the Highlighted db field of the ContentPage that it's copying?

Virtual Pages could be pointed at pages of different types and there is no enforcement that all of those pages are ContentPages, or at least pages that have a Hightlighted db field. You can ensure this manually when you create your SiteTree, but users could come along and screw it up so keep this in mind.
Here is some psuedo-code that might help you get started. It assumes that all virtual pages are ContentPages. If you will have multiple types of VirtualPages referenced by an AreaPage then this is probably not sufficient.
$virtualPages = $myAreaPage->RelatedVirtualPages();
$contentSourcePages = ContentPage::get()->byIDs($virtualPage->column('CopyContentFromID'));
$sortedSourcePages = $contentSourcePages->sort('Highlighted','ASC');
You possibly could also use an innerJoin, but then you have to deal with _Live tables and possibly multiple page tables (again if not just using ContentPage as VirtualPage) which could lead to some complicated scenarios.
Update
So, to summarize in my own words, you need a list of the VirtualContentPages linked to a specific AreaPage sorted on the Highlighted field from the ContentPage that each VirtualContentPage links to. If this summary is accurate, would this work:
$sortedVirtualPages = $myAreaPage->RelatedVirtualPages()
->innerJoin('ContentPage', '"ContentPage"."ID" = "VirtualContentPage"."CopyContentFromID"')
->sort('Highlighted DESC');

I could not find a very clean method, but did find two ways to achieve this. The function goes in the class AreaPage
First
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$highlighted = $items->filterByCallback(function($record, $list) {
if($record->CopyContentFrom() instanceOf ContentPage) {
//return ! $record->CopyContentFrom()->Highlighted; // ASC
return $record->CopyContentFrom()->Highlighted; // DESC
}
});
$highlighted->merge($items);
$highlighted->removeDuplicates();
return $highlighted;
}
Second (the method you described in the comments)
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$arrayList = new ArrayList();
foreach($items as $virtualPage)
{
if($virtualPage->CopyContentFrom() instanceOf ContentPage) {
$virtualPage->Highlighted = $virtualPage->CopyContentFrom()->Highlighted;
$arrayList->push($virtualPage);
}
}
$arrayList = $arrayList->sort('Highlighted DESC');
return $arrayList;
}
I'm not very proud of any of these solutions, but I believe they do fit your criteria.

Here's what I ended up doing, which I think works:
/**
* #return ArrayList
*/
public function VirtualPages()
{
$result = [];
$virtualPages = $this->RelatedVirtualPages();
$contentPages = ContentPage::get()
->byIDs($virtualPages->column('CopyContentFromID'))
->map('ID', 'Highlighted')
->toArray();
foreach($virtualPages as $virtualPage) {
$highlighted = $contentPages[$virtualPage->CopyContentFromID];
$virtualPage->Highlighted = $highlighted;
$result[] = $virtualPage;
}
return ArrayList::create(
$result
);
}
And then it's sortable like so:
$areaPage->VirtualPages()->sort('Highlighted DESC');
Thank you for all the answers and pointers. I'll wait a bit before marking any answer.

Couldn't you just do
//just get one areapage
$AreaPageItem = AreaPage::get()->First();
//now get the RelatedVirtualPages sorted
$related_pages = $AreaPageItem->RelatedVirtualPages()->sort("Highlighted","ASC");

Related

Api platform aliasing filters for nested resources

I'm currently using API Platform and its default SearchFilter and it works as intended.
However, filtering on a deep relationship between resources can be heavy by its quite long query string in the url. (I have got multiple entities like this.)
For instance I want to search every books listed in the stores of a specific country :
{url}/books?department.store.city.country.name=italy
Is there any way to edit the #ApiFilter(SearchFilter::class, properties={}) in order to get simply at the end ?
{url}/books?country_filter=italy
Thanks !
Thank you for your advices,
After some (hours of) researches, I came to the conclusion to extend the SearchFilter when creating my personnal CountryFilter :
In my entity class :
/*
* #ApiFilter(CountryFilter::class, properties={
* "country_filter": "department.store.city.country.name",
* })
*/
In my App\Filter\CountryFilter.php :
<?php
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
final class CountryFilter extends SearchFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
foreach($this->properties as $alias => $propertyName){
if($alias == $property){
$property = $propertyName;
break;
}
}
/*
if (
null === $value ||
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass, true)
) {
return;
}
*/
// The rest of the SearchFilter.php copy/pasted code ...
}
public function getDescription(string $resourceClass): array
{
// ....
}
}
You can make your one custom api filter and add your own logic in it.Call it country_filter and pass one value only, after that a custom query will search in database and return the rows. To make one you have to extend the AbstractFilter class and after that you have to add this filter it in your entity. A good tutorial from official site is here and the next chapter here

how to access annotation of an property(class,mappedBy,inversedBy)

Good morning,
Is it exist an function where I pass an entity and the propertyName and return me the mappedBy,inversedBy and absoluteClassName of an Entity.
The goal is to use the __call to create automatic getteur/setteur and addFucntion bidirectionnal.
I don't want to use generates Entities I want all getteur,setteur and add Function use __call.
But i can"t do an addBirectionnal if i don't know if the relation is many to many or one to many and if i don't know the name of the mappedBy.
my code:
public function __get($p){
return $this->$p;
}
public function __set($p,$v){
$this->$p = $v;
return $this;
}
public function __call($name,$arguments){
if(substr($name,1,3)=='et')
$name2 = substr(3);
if($name[0] == 'g'){
return $this->$name2;
}else{//substr($name,0,1) == 's'
$this->$name2 = $arguments[0];
/*for a one to one*/
/*$mappedByName= getmappedByOrInversedBy(get_class($name),$name2);
if($mappedByName){
$this->$name->$mappedByName = $this;/
}*/
return $this;
}
}
}
I need getmappedByOrInversedBy, thanks.
edit: I try this
public function test(){
$str = "AppBundle\Entity\Group";
$mapping = new \Doctrine\ORM\Mapping\ClassMetadataInfo($str);
$d = $mapping->getAssociationMappedByTargetField('trad');
var_dump($d);
return $this->render('default/index.html.twig', array(
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
));
}
class Group
{
...
/**
* #ORM\OneToOne(targetEntity="Traduction",inversedBy="grp")
*/
protected $trad;
}
Result : Undefined index: trad
The ClassMetadataInfo is what you are looking for.
Creates an instance with the entityName :
$mapping = new \Doctrine\ORM\Mapping\ClassMetadataInfo($entityNamespaceOrAlias);
Then, get the informations you want :
Get all association names: $mapping->getAssociationNames();
Get the join column of an association:
$mapping->getSingleAssociationJoinColumnName($fieldName);
Get the mappedBy column of an association:
$mapping->getAssociationMappedByTargetField($fieldName);
...
Look at the class to know which method you can access.
Hopes it's what you expect.
EDIT
As you can access the EntityManager (i.e. from a controller), use :
$em = $this->getDoctrine()->getManager();
$metadata = $em->getClassMetadata('AppBundle:Group');
To be sure there is no problem with your entity namespace, try :
print $metadata->getTableName();
To retrieve the associations of the entity, use :
$metadata->getAssociationNames();
And to get the mapping informations of an existing association, use :
$metadata->getAssociationMapping($fieldName);
And to get all the association mappings of your entity, use:
$metadata->getAssociationMappings();

SilverStripe convertDataObjectSet is stripping additional properties

I am attempting to add the 'AbsoluteLink' property to each DataObject in a DataList and then convert the list to JSON with JSONDataFormatter::convertDataObjectSet().
I have the following function:
public function json() {
$data = ResourceCentreArticlePage::get()->filter('ShowInMenus', '1')->filter('ShowInSearch', '1')->sort('Created', 'DESC');
$pageArray = new ArrayList();
foreach ($data as $page) {
$page->AbsoluteLink = $page->AbsoluteLink();
$pageArray->push($page);
}
// If I dump out the content of $pageArray here the object has the AbsoluteLink property
$jsonFormatter = new JSONDataFormatter();
$jsonData = $jsonFormatter->convertDataObjectSet($pageArray);
// If I dump out the content of $jsonData here there is no AbsoluteLink property
$this->response->addHeader("Content-type", "application/json");
return $jsonData;
}
The problem:
The AbsoluteLink property is removed after running the $pageArray through the convertDataObjectSet method.
What am I missing?
Using $jsonFormatter->setCustomAddFields(); will help here.
Add the following to the Page class:
public function getMyAbsoluteLink() {
return $this->AbsoluteLink();
}
For example to the Page.php:
class Page extends SiteTree {
public function getMyAbsoluteLink() {
return $this->AbsoluteLink();
}
}
And use that "magic field" like this:
public function json() {
$pages = Page::get()
->filter('ShowInMenus', '1')
->filter('ShowInSearch', '1')
->sort('Created', 'DESC');
$jsonFormatter = new JSONDataFormatter();
// add your custom field
$jsonFormatter->setCustomAddFields(["MyAbsoluteLink"]);
$jsonData = $jsonFormatter->convertDataObjectSet(
$pages
);
return $jsonData;
}
Note the $jsonFormatter->setCustomAddFields(["MyAbsoluteLink"]); and I removed the array manipulation.
Also I removed your array manipulation. How the convertDataobjectSet function works it seems you can't amend the objects before it runs.

Laravel Eloquent hasMany returns null

Laravel 4 is giving me unexpected results on a new project. To try and help me better understand the results I tried what I thought would be a simple exercise and use a friends old WordPress blog on his web hosting.
Post model:
class Post extends Eloquent {
protected $table = 'wp_posts';
public function meta()
{
return $this->hasMany('Meta', 'post_id');
}
}
Meta model
class Meta extends Eloquent {
protected $table = 'wp_postmeta';
public function post()
{
return $this->belongsTo('Post');
}
}
I have tried all of these variations with no avail...
TestController:
public function get_index()
{
// $test = Post::with('Meta')->get();
// $test = Post::with('Meta')->where('id', '=', '219')->first();
// $test = Post::find(219)->Meta()->where('post_id', '=', '219')->first();
// $test = Post::find($id)->Meta()->get();
// $test = Meta::with('Post')->get();
$id = 219;
$test = Post::find($id)->meta;
return $test;
}
returned in the event listener:
string(47) "select * from `wp_posts` where `id` = ? limit 1" string(65) "select * from `wp_postmeta` where `wp_postmeta`.`post_id` is null" []
Please tell me I am just overlooking something really minor and stupid and I just need some sleep.
Though is that while the SQL is case-insensitive, the array of attributes the model gets populated with is a PHP array where indexes are case-sensitive. The schema had the primary key as ID

Symfony2 data transformer, validator and error message

I asked this question and found out that we can't get the error message thrown by a DataTransformer (according to the only user who answered, maybe it's possible, I don't know).
Anyway, now that I know that, I am stucked with a problem of validation. Suppose my model is this one: I have threads that contains several participants (users).
<?php
class Thread
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="My\UserBundle\Entity\User")
* #ORM\JoinTable(name="messaging_thread_user")
*/
private $participants;
// other fields, getters, setters, etc
}
For thread creation, I want the user to specify the participants usernames in a textarea, separated by "\n".
And I want that if one or more of the usernames specified don't exist, a message is displayed with the usernames that don't exist.
For example, "Users titi, tata and toto don't exist".
For that I created a DataTransformer that transforms the raw text in the textarea into an ArrayCollection containing instances of users. Since I can't get the error message provided by this DataTransformer (such a shame! Is it really impossible?), I don't check the existence of each usernames in the DataTransformer but in the Validator.
Here is the DataTransformer that converts \n-separated user list into an ArrayCollection (so that the DataBinding is ok):
<?php
public function reverseTransform($val)
{
if (empty($val)) {
return null;
}
$return = new ArrayCollection();
// Extract usernames in an array from the raw text
$val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
foreach ($usernames as $username) {
$user = new User();
$user->setUsername($username);
if (!$return->contains($user)) {
$return->add($user);
}
}
return $return;
}
And here is my validator:
<?php
public function isValid($value, Constraint $constraint)
{
$repo = $this->em->getRepository('MyUserBundle:User');
$notValidUsernames = array();
foreach ($value as $user) {
$username = $user->getUsername();
if (!($user = $repo->findOneByUsername($username))) {
$notValidUsernames[] = $username;
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
// Create the list of usernames separated by commas
$list = '';
$i = 1;
foreach ($notValidUsernames as $username) {
if ($i < count($notValidUsernames)) {
$list .= $username;
if ($i < count($notValidUsernames) - 1) {
$list .= ', ';
}
}
$i++;
}
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $list,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
This current implementation looks ugly. I can see the error message well, but the users in the ArrayCollection returned by the DataTransformer are not synchronized with Doctrine.
I got two questions:
Is there any way that my validator could modify the value given in parameter? So that I can replace the simple User instances in the ArrayCollection returned by the DataTransformer into instances retrieved from the database?
Is there a simple and elegant way to do what I'm doing?
I guess the most simple way to do this is to be able to get the error message given by the DataTransformer. In the cookbook, they throw this exception: throw new TransformationFailedException(sprintf('An issue with number %s does not exist!', $val));, if I could put the list of non-existing usernames in the error message, it would be cool.
Thanks!
I am the one that answered your previous thread so maybe someone else will jump in here.
Your code can be simplified considerably. You are only dealing with user names. No need for use objects or array collections.
public function reverseTransform($val)
{
if (empty($val)) { return null; }
// Extract usernames in an array from the raw text
// $val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
// No real need to check for dups here
return $usernames;
}
The validator:
public function isValid($userNames, Constraint $constraint)
{
$repo = $this->em->getRepository('SkepinUserBundle:User');
$notValidUsernames = array();
foreach ($userNames as $userName)
{
if (!($user = $repo->findOneByUsername($username)))
{
$notValidUsernames[$userName] = $userName; // Takes care of dups
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
$invalidNames = implode(' ,',$notValidUsernames);
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $invalidNames,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
=========================================================================
So at this point
We have used transformer to copy the data from the text area and generated an array of user names during form->bind().
We then used a validator to confirm that each user name actually exists in the database. If there are any that don't then we generate an error message and form->isValid() will fail.
So now we are back in the controller, we know we have a list of valid user names (possibly comma delimited or possibly just an array). Now we want to add these to our thread object.
One way would to create a thread manager service and add this functionality to it. So in the controller we might have:
$threadManager = $this->get('thread.manager');
$threadManager->addUsersToThread($thread,$users);
For the thread manager we would inject our entity manager. In the add users method we would get a reference to each of the users, verify that the thread does not already have a link to this user, call $thread->addUser() and then flush.
The fact that we have wrapped up this sort of functionality into a service class will make things easier to test as we can also make a command object and run this from the command line. it also gives us a nice spot to add additional thread related functionality. We might even consider injecting this manager into the user name validator and moving some of the isValid code to the manager.

Resources