Doctrine 2 - Association table with additional properties & inheritance - symfony

I'm just starting with Doctrine (no experience with ORMs so far) on my first Symfony2 project.
I have a pretty simple structure, but am not satisfied with how classes are generated.I looked for tutorials/related questions on stackoverflow, but I haven't found a comprehensive answer so far :
Let say I have 2 entities + an association table with additional properties :
User
- id: int
- name: string
- movies: UserMovie
Movie
- id: int
- name: string
- duration: int
UserMovie:
- user_id: int
- movie_id: int
- seen: bool
A user can have several movies / a movie can be owned by several users.
What I would like is to have the generated UserMovie class inheriting from Movie, so I can access movies properties from a UserMovie instance directly.
I'd like to get something like this as a result of the generation process:
class UserMovie extends Movie
{
protected $user; // User instance
protected $id; // inherited from Movie
protected $name; // inherited from Movie
protected $duration; // inherited from Movie
}
Is this even possible?
Is there some best practices in this case?
Not sure this is clear enough (as I said earlier, I'm pretty new to this), any help would be gladly appreciated :)

It is not clear enough :)
You should build
User hasMany UserMovies
Movie hasMany UserMovies
relations. That will allow you to do something like this
var_dump( $user->hasMovie($movie) ) ;
by doing
class User
{
public function hasMovie(Movie $movie)
{
foreach($this->usermovies as $m2m) {
if ( $m2m->getMovie() === $movie )
return true ;
}
return false ;
}
public function addMovie(Movie $movie)
{
if ( !$this->hasMovie($movie) ) {
$m2m = new UserMovie() ;
$m2m->setMovie($movie) ;
$m2m->setUser($this) ;
$this->usermovies->add($m2m) ;
}
}
}
or alike

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

Symfony 4: Returning only active records

I would like to only get records where the active indicator is true:
class Question {
/**
* One Question has one Figure
* #ORM\OneToOne(targetEntity="QuestionFigure", mappedBy="question")
*/
private $figure;
public function getFigure()
{
$criteria = Criteria::create()->where(Criteria::expr()->eq("active", true));
return $this->figure->matching($criteria);
}
When I do this, I get the error:
Attempted to call an undefined method named "matching" of class
I believe this is because the matching method can only be applied to an ArrayCollection which $this->figure is not. What would be a similar way of achieving this same result?
Edit based on answer provided by Ihor Kostrov:
getActive() is returning nothing. Testing this out, this works:
public function getFigure()
{
if (!empty($this->figure) && $this->figure->getId() === 1) {
return $this->figure;
}
return null;
}
But changing the id to 2 does not work ($this->figure->getId() === 2). I am thinking this is because of the one-to-one relationship doctrine only fetches one row?
Yes, you have OneToOne, so you cat try this
public function getFigure(): ?QuestionFigure
{
if (!empty($this->figure) && $this->figure->getActive()) {
return $this->figure;
}
return null;
}
No! You must not have logic in your entity!
To have all question with have QuestionFigure active you must configure a repository and you implement a method.

How to skip displaying a content item in Orchard CMS?

I have a content part that provides a begin timestamp and end timestamp option. These 2 fields are used to define a period of time in which the content item should be displayed.
I now have difficulties to implement a skip approach whereas content items should not be displayed / skipped when the period of time does not span the current time.
Digging in the source code and trying to find an entry point for my approach resulted in the following content handler
public class SkipContentHandler : Orchard.ContentManagement.Handlers.ContentHandler
{
protected override void BuildDisplayShape(Orchard.ContentManagement.Handlers.BuildDisplayContext aContext)
{
if (...) // my condition to process only content shapes which need to be skipped
{
aContext.Shape = null; // return null shape to skip it
}
}
}
This works but there are several side effects
I had to alter the source code of BuildDisplayContext as the Shape is normally read only
List shape may displayed a wrong pager when it contains content items with my content part because the Count() call in ContainerPartDriver.Display() is executed before BuildDisplay()
calling the URL of a content item that is skipped results in an exception because View(null) is abigious
So, what would be the correct approach here or is there any module in existence that does the job? I couldn't find one.
This is a quite complex task. There are several steps needed to achieve a proper skipping of display items:
Create the part correctly
There are a few pitfalls here as when coming to the task of adding a part view one might utilize Orchards date time editor in connection with the DateTime properties. But this brings a heck of a lot of additional issues to the table but these don't really relate to the question.
If someone is interested in how to use Orchards date time editor then i can post this code too, but for now it would only blow up the code unnecessarly.
So here we go, the part class...
public class ValidityPart : Orchard.ContentManagement.ContentPart<ValidityPartRecord>
{
// public
public System.DateTime? ValidFromUtc
{
get { return Retrieve(r => r.ValidFromUtc); }
set { Store(r => r.ValidFromUtc, value); }
}
...
public System.DateTime? ValidTillUtc
{
get { return Retrieve(r => r.ValidTillUtc); }
set { Store(r => r.ValidTillUtc, value); }
}
...
public bool IsContentItemValid()
{
var lUtcNow = System.DateTime.UtcNow;
return (ValidFromUtc == null || ValidFromUtc.Value <= lUtcNow) && (ValidTillUtc == null || ValidTillUtc.Value >= lUtcNow);
}
...
}
...and the record class...
public class ValidityPartRecord : Orchard.ContentManagement.Records.ContentPartRecord
{
// valid from value as UTC to use Orchard convention (see CommonPart table) and to be compatible with projections
// (date/time tokens work with UTC values, see https://github.com/OrchardCMS/Orchard/issues/6963 for a related issue)
public virtual System.DateTime? ValidFromUtc { get; set; }
// valid from value as UTC to use Orchard convention (see CommonPart table) and to be compatible with projections
// (date/time tokens work with UTC values, see https://github.com/OrchardCMS/Orchard/issues/6963 for a related issue)
public virtual System.DateTime? ValidTillUtc { get; set; }
}
Create a customized content query class
public class MyContentQuery : Orchard.ContentManagement.DefaultContentQuery
{
// public
public ContentQuery(Orchard.ContentManagement.IContentManager aContentManager,
Orchard.Data.ITransactionManager aTransactionManager,
Orchard.Caching.ICacheManager aCacheManager,
Orchard.Caching.ISignals aSignals,
Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentTypeRecord> aContentTypeRepository,
Orchard.IWorkContextAccessor aWorkContextAccessor)
: base(aContentManager, aTransactionManager, aCacheManager, aSignals, aContentTypeRepository)
{
mWorkContextAccessor = aWorkContextAccessor;
}
protected override void BeforeExecuteQuery(NHibernate.ICriteria aContentItemVersionCriteria)
{
base.BeforeExecuteQuery(aContentItemVersionCriteria);
// note:
// this method will be called each time a query for multiple items is going to be executed (e.g. content items of a container, layers, menus),
// this gives us the chance to add a validity criteria
var lWorkContext = mWorkContextAccessor.GetContext();
// exclude admin as content items should still be displayed / accessible when invalid as validity needs to be editable
if (lWorkContext == null || !Orchard.UI.Admin.AdminFilter.IsApplied(lWorkContext.HttpContext.Request.RequestContext))
{
var lUtcNow = System.DateTime.UtcNow;
// left outer join of ValidityPartRecord table as part is optional (not present on all content types)
var ValidityPartRecordCriteria = aContentItemVersionCriteria.CreateCriteria(
"ContentItemRecord.ValidityPartRecord", // string adopted from foreach loops in Orchard.ContentManagement.DefaultContentQuery.WithQueryHints()
NHibernate.SqlCommand.JoinType.LeftOuterJoin
);
// add validity criterion
ValidityPartRecordCriteria.Add(
NHibernate.Criterion.Restrictions.And(
NHibernate.Criterion.Restrictions.Or(
NHibernate.Criterion.Restrictions.IsNull("ValidFromUtc"),
NHibernate.Criterion.Restrictions.Le("ValidFromUtc", lUtcNow)
),
NHibernate.Criterion.Restrictions.Or(
NHibernate.Criterion.Restrictions.IsNull("ValidTillUtc"),
NHibernate.Criterion.Restrictions.Ge("ValidTillUtc", lUtcNow)
)
)
);
}
}
// private
Orchard.IWorkContextAccessor mWorkContextAccessor;
}
This essentially adds a left join of the validity part fields to the SQL query (content query) and extends the WHERE statement with the validity condition.
Please note that this step is only possible with the solution described the following issue: https://github.com/OrchardCMS/Orchard/issues/6978
Register the content query class
public class ContentModule : Autofac.Module
{
protected override void Load(Autofac.ContainerBuilder aBuilder)
{
aBuilder.RegisterType<MyContentQuery>().As<Orchard.ContentManagement.IContentQuery>().InstancePerDependency();
}
}
Create a customized content manager
public class ContentManager : Orchard.ContentManagement.DefaultContentManager
{
// public
public ContentManager(
Autofac.IComponentContext aContext,
Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentTypeRecord> aContentTypeRepository,
Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentItemRecord> aContentItemRepository,
Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentItemVersionRecord> aContentItemVersionRepository,
Orchard.ContentManagement.MetaData.IContentDefinitionManager aContentDefinitionManager,
Orchard.Caching.ICacheManager aCacheManager,
System.Func<Orchard.ContentManagement.IContentManagerSession> aContentManagerSession,
System.Lazy<Orchard.ContentManagement.IContentDisplay> aContentDisplay,
System.Lazy<Orchard.Data.ITransactionManager> aTransactionManager,
System.Lazy<System.Collections.Generic.IEnumerable<Orchard.ContentManagement.Handlers.IContentHandler>> aHandlers,
System.Lazy<System.Collections.Generic.IEnumerable<Orchard.ContentManagement.IIdentityResolverSelector>> aIdentityResolverSelectors,
System.Lazy<System.Collections.Generic.IEnumerable<Orchard.Data.Providers.ISqlStatementProvider>> aSqlStatementProviders,
Orchard.Environment.Configuration.ShellSettings aShellSettings,
Orchard.Caching.ISignals aSignals,
Orchard.IWorkContextAccessor aWorkContextAccessor)
: base(aContext, aContentTypeRepository, aContentItemRepository, aContentItemVersionRepository, aContentDefinitionManager, aCacheManager, aContentManagerSession,
aContentDisplay, aTransactionManager, aHandlers, aIdentityResolverSelectors, aSqlStatementProviders, aShellSettings, aSignals)
{
mWorkContextAccessor = aWorkContextAccessor;
}
public override ContentItem Get(int aId, Orchard.ContentManagement.VersionOptions aOptions, Orchard.ContentManagement.QueryHints aHints)
{
var lResult = base.Get(aId, aOptions, aHints);
if (lResult != null)
{
// note:
// the validity check is done here (after the query has been executed!) as changing base.GetManyImplementation() to
// apply the validity critera directly to the query (like in ContentQuery) will not work due to a second attempt to retrieve the
// content item from IRepository<> (see base.GetManyImplementation(), comment "check in memory") when the query
// returns no data (and the query should not return data when the validity critera is false)
//
// http://stackoverflow.com/q/37841249/3936440
var lWorkContext = mWorkContextAccessor.GetContext();
// exclude admin as content items should still be displayed / accessible when invalid as validity needs to be editable
if (lWorkContext == null || !Orchard.UI.Admin.AdminFilter.IsApplied(lWorkContext.HttpContext.Request.RequestContext))
{
var lValidityPart = lResult.As<ValidityPart>();
if (lValidityPart != null)
{
if (lValidityPart.IsContentItemValid())
{
// content item is valid
}
else
{
// content item is not valid, return null (adopted from base.Get())
lResult = null;
}
}
}
}
return lResult;
}
// private
Orchard.IWorkContextAccessor mWorkContextAccessor;
}
Steps 2-4 are needed when having content items whereas the content type has a Container and Containable part or even content items which are processed / displayed separately. Here you normally cannot customize the content query that is executed behind the scenes.
Steps 2-4 are not needed if you use the Projection module. But again, this brings a few other issues to the table as reported in this issue: https://github.com/OrchardCMS/Orchard/issues/6979

Symfony2 Nested forms and ManyToMany relations

I'm trying to handle a form which is quite complex for me...
We have Collections, which contain books (OneToMany), with articles(OneToMany) and their authors (ManyToMany).
The user can edit a book: he can add or remove an article, and add or remove some authors for each article. There are nested forms : book>article>author.
If the author is new in the Collection, it is created for that Collection.
Entities descriptions look fine, database is generated by the console and seems consistent.
This is working fine if I don't have to deal with authors using the book edition form. If the author exists, I have a duplicate entry bug. If the author is new, I have a "Explicitly persist the new entity or configure cascading persist operations on the relationship" bug.
Here is the code:
public function onSuccess(Book $book)
{
$this->em->persist($book);
foreach($book->getArticles() as $article)
{
$article->setUrlname($this->mu->generateUrlname($article->getName()));
$article->setBook($book);
// Saving (and creating) the authors of the book
foreach ($this->collectionWithAuthors->getAuthors() as $existAuthor){
foreach($article->getAuthors() as $author) {
$authorUrlname=$this->mu->generateUrlname($author->getFirstname().' '.$author->getLastname());
if ( $existAuthor->getUrlname() == $authorUrlname) { // The author is existing
$article->addAuthor($existAuthor);
$this->em->persist($existAuthor);
}else{ // New Author
$newAuthor = new Author();
$newAuthor->setCollection($this->collectionWithBaseArticles);
$newAuthor->setLastname($author->getLastname());
$newAuthor->setFirstname($author->getFirstname());
$newAuthor->setUrlname($authorUrlname);
$this->em->persist($newAuthor);
$article->addAuthor($newAuthor);
}
}
}
$this->em->persist($article);
}
$this->em->flush();
}
I don't know how to use cascades. But the $article->addAuthor() is supposed to call $authors->addArticle():
Article Entity extract
/**
* #ORM\ManyToMany(targetEntity="bnd\myBundle\Entity\Author", mappedBy="articles")
*/
private $authors;
/**
* Add authors
*
* #param bnd\myBundle\Entity\Author $authors
* #return Article
*/
public function addAuthor(\bnd\myBundle\Entity\Author $authors)
{
$this->authors[] = $authors;
$authors->addArticle($this);
}
The logic in foreach statement is wrong. Suppose we have next authors:
Persisted authors (collectionWithAuthors):
John
Eric
Submitted authors
Ada
Eric
So for every existing author (John and Eric) the script loop thru new authors:
foreach ([John, Eric] as $author) {
foreach([Ada, Eric] as $newAuthor) {
// John author: the script persist Ada(right) and Eric(wrong) as new authors
// Eric author: the script persist Ada(wrong), but not Eric(right)
}
}
The solution is to replace article authors with existing authors (if there is similar)
foreach ($article->getAuthors() as $key => $articleAuthor) {
$authorUrlname=$this->mu->generateUrlname($articleAuthor->getFirstname().' '.$articleAuthor->getLastname());
$foundAuthor = false;
// Compare article author with each existing author
foreach ($this->collectionWithAuthors->getAuthors() as $existAuthor) {
if ($existAuthor->getUrlname() == $authorUrlname) {
$foundAuthor = true;
break; // It has found similar author no need to look further
}
}
// Use $existAuthor as found one, otherwise use $articleAuthor
if ($foundAuthor) {
$article->removeAuthor($articleAuthor); // Remove submitted author, so he wont be persisted to database
$article->addAuthor($existAuthor);
} else {
// Here you dont need to create new author
$articleAuthor->setCollection($this->collectionWithBaseArticles);
$articleAuthor->setUrlname($authorUrlname);
}
...
}
$this->_em->persist($article);
You have noticed i removed any author persistent from the loop, to persist these authors its better to set cascade={'persist'} in $authors annotation of Article Entity Class
/**
* #ORM\ManyToMany(targetEntity="Author", cascade={"persist"})
* #ORM\JoinTable(...)
*/
protected $authors;
UPD:
I forgot to mention one thing about cascade persistent. To persist relation betweed article and author you also have to add relation to author entity. Edit the addAuthor() method in the Article entity as below:
public function addAuthor(Author $author)
{
// Only add author relation if the article does not have it already
if (!$this->authors->contains($author)) {
$this->authors[] = $author;
$author->addArticle($this);
}
}
Also, it's a good practice to define default values for a collection of entities in the constructor:
use Doctrine\Common\Collections\ArrayCollection;
// ...
public function __construct()
{
$this->authors = new ArrayCollection();
}

Get all descendants types of base class

I have a base class called BaseEvent and several descendants classes:
public class BaseEvent {
// the some properties
// ...
}
[MapInheritance(MapInheritanceType.ParentTable)]
public class Film : BaseEvent {
// the some properties
// ...
}
[MapInheritance(MapInheritanceType.ParentTable)]
public class Concert : BaseEvent {
// the some properties
// ...
}
I have a code which create the BaseEvent instance at runtime:
BaseEvent event = new BaseEvent();
// assign values for a properties
// ...
baseEvent.XPObjectType = Database.XPObjectTypes.SingleOrDefault(
t => t.TypeName == "MyApp.Module.BO.Events.BaseEvent");
Now, this event will be shows in BaseEvent list view.
I want to do the following: when a user click Edit button then show in list view lookup field with all descendants types. And when user saves record change ObjectType to selected value.
How can I do this?
Thanks.
PS. this is asp.net app.
I'm not sure that your approach is correct for what you are trying to achieve. First, I'll answer the question you have asked, and afterwards I'll try to explain how the XAF already provides the functionality you are trying to achieve, namely how to choose which subclass of record to create from the user interface.
In order to create a property which allows the user to choose a Type within the application, you can declare a TypeConverter:
public class EventClassInfoTypeConverter : LocalizedClassInfoTypeConverter
{
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
List<Type> values = new List<Type>();
foreach (ITypeInfo info in XafTypesInfo.Instance.PersistentTypes)
{
if ((info.IsVisible && info.IsPersistent) && (info.Type != null))
{
// select BaseEvent subclasses
if (info.Type.IsSubclassOf(typeof(BaseEvent)))
values.Add(info.Type);
}
}
values.Sort(this);
values.Insert(0, null);
return new TypeConverter.StandardValuesCollection(values);
}
}
And then your base event class would look like:
public class BaseEvent: XPObject
{
public BaseEvent(Session session)
: base(session)
{ }
private Type _EventType;
[TypeConverter(typeof(EventClassInfoTypeConverter))]
public Type EventType
{
get
{
return _EventType;
}
set
{
SetPropertyValue("EventType", ref _EventType, value);
}
}
}
However, I suspect this is not the functionality you require. Modifying the value of the property will NOT change the base type of the record. That is, you will end up with a record of type BaseEvent which has a property Type equal to 'Concert' or 'Film'.
XAF already provides a mechanism for selecting the type of record to create. In your scenario, you will find that the New button is a dropdown with your different subclasses as options:
Therefore you do not need to create a 'type' property within your object. If you need a column to show the type of event in the list view, you can declare a property as follows
[PersistentAlias("XPObjectType.Name")]
public string EventType
{
get
{
return base.ClassInfo.ClassType.Name;
}
}

Resources