I have a multi step form and need to store my entity in the session.The problem is that the entity have relations and when I retrieve it from Session, the relations attributes are disappearing (null).
I know I can serialize the entity and deserialize it, but it's a lot of checks/code with many relations...
public function route1(Session $session, ....)
{
$entity = new Entity();
$form = $this->createForm(EntityType::class, $entity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$session->set('entityData', $entity);
dump($session->get('entityData')); // Here it's fine, nested attributes are still set
return $this->redirectToRoute('route2');
}
}
public function route2(Session $session)
{
$entity = $this->em->merge($session->get('entityData'));
dump($entity, $entity->getCustomer()->getFistname()); // And here all nested attributes are null like on the image ...
}
Related
I have a problem using Doctrine with many to many relation when I persist my data.
I have 2 entities :
- Bloc
- Job
I retrieve my ID's before the persist with getJobs() method.
$bloc = $form->getData();
// $bloc->getJobs() works I retrieve good IDs with foreach getId()
$em->persist($bloc);
$em->flush();
My method addJob :
/**
* Add jobs
*
* #param Job $jobs
* #return Bloc
*/
public function addJob(Job $jobs)
{
$this->jobs[] = $jobs;
return $this;
}
My form:
$bloc = $em->getRepository('Acme\\Entity\\Bloc')->find($id);
$form = $this->createForm(BlocType::class, $bloc);
$form->handleRequest();
if($form->isSubmitted()) {
$bloc = $form->getData();
$em->persist($bloc);
$em->flush();
$this->addFlashMessage('edit', "Update message");
}
But the persist add new line in my job entity instead of use line already in my database.
Any idea?
I guess $jobs is a Collection of Job, so you should implements methods to handle this collection.
If your entity declarations are correct (oneToMany, ManyToOne, uni/bi directional) you should have something like this :
class Bloc
{
// ...
protected $jobs;
public function __construct()
{
$this->jobs = new Doctrine\Common\Collections\ArrayCollection();
}
public function getJobs()
{
return $this->jobs;
}
public function setJobs($jobs)
{
$this->jobs = new Doctrine\Common\Collections\ArrayCollection();
foreach ($jobs as $job) {
$this->addJob($job);
}
return $this;
}
public function addJob(Job $job)
{
if (!$this->jobs->contains($job) {
$this->jobs->add($job);
// if needed for bi directional way
// $job->setBloc($this);
}
}
public function removeJob(Job $job)
{
// if you want
}
// ...
}
The glass mapper will return null object or (no items) for SitecoreQuery and SitecoreChildren attribute that are placed on the GlassModels. These attributes don't take any such parameter where I can specify them to return items if they don't exist in the the context lanaguge. The items e.g. exist in EN but don't exist in en-ES. I need to put a lot of null check in my views to avoid Null exception and makes the views or controller very messy. It is lot of boiler plate code that one has to write to make it work.
In Page Editor the SitecoreChildren returns item and content authors can create items in that langauge version by editing any field on the item. This automatically creates the item in that langauge. However the same code will fail in Preview mode as SitecoreChidren will return null and you see null pointer exception.
SitecoreQuery doesn't return any items in page editor and then Content Authors wont be able to create items in Page editor.
To make the experience good if we can pass a parameter to SiteocreQuery attribute so it disable VsersionCount and returns the items if they dont exist in that langauge.
This is actually not possible. There is an issue on GitHub which would make it easy to create a custom attribute to handle this very easy. Currently you need to create a new type mapper and copy all the code from the SitecoreQueryMapper. I have written a blog post here about how you can create a custom type mapper. You need to create the following classes (example for the SitecoreQuery).
New configuration:
public class SitecoreSharedQueryConfiguration : SitecoreQueryConfiguration
{
}
New attribute:
public class SitecoreSharedQueryAttribute : SitecoreQueryAttribute
{
public SitecoreSharedQueryAttribute(string query) : base(query)
{
}
public override AbstractPropertyConfiguration Configure(PropertyInfo propertyInfo)
{
var config = new SitecoreSharedQueryConfiguration();
this.Configure(propertyInfo, config);
return config;
}
}
New type mapper:
public class SitecoreSharedQueryTypeMapper : SitecoreQueryMapper
{
public SitecoreSharedQueryTypeMapper(IEnumerable<ISitecoreQueryParameter> parameters)
: base(parameters)
{
}
public override object MapToProperty(AbstractDataMappingContext mappingContext)
{
var scConfig = Configuration as SitecoreQueryConfiguration;
var scContext = mappingContext as SitecoreDataMappingContext;
using (new VersionCountDisabler())
{
if (scConfig != null && scContext != null)
{
string query = this.ParseQuery(scConfig.Query, scContext.Item);
if (scConfig.PropertyInfo.PropertyType.IsGenericType)
{
Type outerType = Glass.Mapper.Sc.Utilities.GetGenericOuter(scConfig.PropertyInfo.PropertyType);
if (typeof(IEnumerable<>) == outerType)
{
Type genericType = Utilities.GetGenericArgument(scConfig.PropertyInfo.PropertyType);
Func<IEnumerable<Item>> getItems;
if (scConfig.IsRelative)
{
getItems = () =>
{
try
{
return scContext.Item.Axes.SelectItems(query);
}
catch (Exception ex)
{
throw new MapperException("Failed to perform query {0}".Formatted(query), ex);
}
};
}
else
{
getItems = () =>
{
if (scConfig.UseQueryContext)
{
var conQuery = new Query(query);
var queryContext = new QueryContext(scContext.Item.Database.DataManager);
object obj = conQuery.Execute(queryContext);
var contextArray = obj as QueryContext[];
var context = obj as QueryContext;
if (contextArray == null)
contextArray = new[] { context };
return contextArray.Select(x => scContext.Item.Database.GetItem(x.ID));
}
return scContext.Item.Database.SelectItems(query);
};
}
return Glass.Mapper.Sc.Utilities.CreateGenericType(typeof(ItemEnumerable<>), new[] { genericType }, getItems, scConfig.IsLazy, scConfig.InferType, scContext.Service);
}
throw new NotSupportedException("Generic type not supported {0}. Must be IEnumerable<>.".Formatted(outerType.FullName));
}
{
Item result;
if (scConfig.IsRelative)
{
result = scContext.Item.Axes.SelectSingleItem(query);
}
else
{
result = scContext.Item.Database.SelectSingleItem(query);
}
return scContext.Service.CreateType(scConfig.PropertyInfo.PropertyType, result, scConfig.IsLazy, scConfig.InferType, null);
}
}
}
return null;
}
public override bool CanHandle(AbstractPropertyConfiguration configuration, Context context)
{
return configuration is SitecoreSharedQueryConfiguration;
}
}
And configure the new type mapper in your glass config (mapper and parameters for the constructor):
container.Register(Component.For<AbstractDataMapper>().ImplementedBy<SitecoreSharedQueryTypeMapper>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemPathParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemIdParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemIdNoBracketsParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemEscapedPathParameter>>().LifeStyle.Transient);
container.Register(Component.For<IEnumerable<ISitecoreQueryParameter>>().ImplementedBy<List<ItemDateNowParameter>>().LifeStyle.Transient);
You can then simply change the SitecoreQuery attribute on your model to SitecoreSharedQuery:
[SitecoreSharedQuery("./*")]
public virtual IEnumerable<YourModel> YourItems { get; set; }
For the children you could either use the shared query mapper and querying the children or create the same classes for a new SitecoreSharedChildren query.
Edit: Added bindings for IEnumerable<ISitecoreQueryParameter> as they are missing and therefor it threw an error.
This question is one in a series I seem to be generating as I slowly pick my way through learning testing.
I have a book model and a ticketaudit model. They have a relationship one to many. When a book is created a function should also create a range of tickets (for audit).
I want my test to make sure the ticketAudit model is being created and the association being made using the eloquent ORM within laravel.
my class so far:
Class TicketCreator implements TicketCreatorInterface {
protected $ticket;
public function __construct(TicketAudit $ticketAudit)
{
//dd($ticketAudit);
$this->ticket = $ticketAudit;
}
public function createTicket($input, $book) {
$counter = $input['start'];
while($counter <= $input['end']) {
$ticketDetails = array(
'ticketnumber'=>$counter,
'status'=>'unused',
'active'=>1
);
$this->ticket->create($ticketDetails)->save();
$this->ticket->book()->associate($book)->save();
$counter = $counter+1;
}
return $counter;
}
}
and my attempts at a test:
public function testCreateCreatesTickets() {
//arrange
$book = FactoryMuff::create('Book');
$aTicket = FactoryMuff::create('TicketAudit');
$ticketAudit = new TicketAudit;
$ticketCreator = new TicketCreator($ticketAudit);
//act
$response = $ticketCreator->createTicket(array('start'=>1000, 'end'=>1001), $book);
// Assert...
$this->assertEquals(true, $response);
}
However when I run this test I get the error:
Integrity constraint violation: 19 ticket_audits.ticketnumber may not be NULL
For some reason the model is not being created with the values I pass to it. I've checked in the function that the object exists and also the values are being created correctly in the array but it doesnt work.
Is this unique to testing?
I am creating an sqlite in memory database for this test.
Any help appreciated
Crikey this decision to start testing is a bit of a nightmare
Thanks to Manuel's request to post the TicketAudit class I noticed my model extended Eloquent. I had recently added Ardent and should have extended Ardent so the error lay in the model!
Revised corrected model :
use LaravelBook\Ardent\Ardent;
class TicketAudit extends Ardent {
protected $guarded = array();
public $autoHydrateEntityFromInput = true;
public $autoPurgeRedundantAttributes = true;
public static $rules = array(
'status' => 'required',
'ticketnumber' => 'required'
);
public static $factory = array(
'ticketnumber' => '1000',
'status' => 'unused',
);
public function book() {
return $this->belongsTo('Book');
}
}
Thank you for the sign post
I've figured out that I can clone an Ember Data record and copy its Attributes, but none of the belongsTo/hasMany relationships are cloned. Can I do this somehow if I don't know what relationships would be possible, going off of the relationships that exist?
For reference, here is what I've got that will clone an Ember Data record's attributes:
var attributeKeys = oldModel.get('constructor.attributes.keys.list');
var newRecord = this.get('store').createRecord(oldModel.constructor.typeKey);
newRecord.setProperties(oldModel.getProperties(attributeKeys));
A few improvements to Daniel's answer that allows providing overrides, and clears the id for new records, to be safe. Also I prefer not to call save within the clone method, but leave it up to the caller.
DS.Model.reopen({
clone: function(overrides) {
var model = this,
attrs = model.toJSON(),
class_type = model.constructor;
var root = Ember.String.decamelize(class_type.toString().split('.')[1]);
/*
* Need to replace the belongsTo association ( id ) with the
* actual model instance.
*
* For example if belongsTo association is project, the
* json value for project will be: ( project: "project_id_1" )
* and this needs to be converted to ( project: [projectInstance] )
*/
this.eachRelationship(function(key, relationship) {
if (relationship.kind == 'belongsTo') {
attrs[key] = model.get(key);
}
});
/*
* Need to dissociate the new record from the old.
*/
delete attrs.id;
/*
* Apply overrides if provided.
*/
if (Ember.typeOf(overrides) === 'object') {
Ember.setProperties(attrs, overrides);
}
return this.store.createRecord(root, attrs);
}
});
Here is a clone function that I use. Takes care of the belongs to associations.
DS.Model.reopen({
clone: function() {
var model = this,
attrs = model.toJSON(),
class_type = model.constructor;
var root = Ember.String.decamelize(class_type.toString().split('.')[1]);
/**
* Need to replace the belongsTo association ( id ) with the
* actual model instance.
*
* For example if belongsTo association is project, the
* json value for project will be: ( project: "project_id_1" )
* and this needs to be converted to ( project: [projectInstance] )
*
*/
this.eachRelationship(function(key, relationship){
if (relationship.kind == 'belongsTo') {
attrs[key] = model.get(key)
}
})
return this.store.createRecord(root, attrs).save();
}
})
There's an addon called ember-cli-copyable that according to its description:
Deeply copies your records including their relations. The mixin is smart enough to resolve not loaded relations and is configurable to what should be shallow/deeply copied or excluded entirely.
Here is the simple way to clone your Ember Model with relationships.
working fine.
Create a Copyable mixin like,
import Ember from 'ember';
export default Ember.Mixin.create(Ember.Copyable, {
copy(deepClone) {
var model = this, attrs = model.toJSON(), class_type = model.constructor;
var root = Ember.String.decamelize(class_type.toString().split(':')[1]);
if(deepClone) {
this.eachRelationship(function(key, relationship){
if (relationship.kind == 'belongsTo') {
attrs[key] = model.get(key).copy(true);
} else if(relationship.kind == 'hasMany' && Ember.isArray(attrs[key])) {
attrs[key].splice(0);
model.get(key).forEach(function(obj) {
attrs[key].addObject(obj.copy(true));
});
}
});
}
return this.store.createRecord(root, attrs);
}
});
Add the mixin in your model,
Note: If you want to clone your child model then, you need to include the mixin in child model as well
USAGE:
With relationship : YOURMODEL.copy(true)
Without relationship : YOURMODEL.copy()
In Symfony2 RC3, I am trying to create a related entity on a User object (FOSUserBundle) at the point of user creation so that I can display the appropriate fields on an edit profile form. I am doing the following in the RegistrationFormHandler.
class RegistrationFormHandler
{
protected $request;
protected $userManager;
protected $form;
public function __construct(Form $form, Request $request, UserManagerInterface $userManager)
{
$this->form = $form;
$this->request = $request;
$this->userManager = $userManager;
}
public function process($confirmation = null)
{
$user = $this->userManager->createUser();
$this->form->setData($user);
if ('POST' == $this->request->getMethod()) {
$this->form->bindRequest($this->request);
if ($this->form->isValid()) {
if (true === $confirmation) {
$user->setEnabled(false);
} else if (false === $confirmation) {
$user->setConfirmationToken(null);
$user->setEnabled(true);
}
$prog = new \MyBundle\CoreBundle\Entity\Programme();
$prog->setStartDate(date_create());
$prog->setEndDate(date_create());
$prog->setWeeklyTarget(4);
$prog->setGoal('');
$user->addProgrammes($prog);
$this->userManager->updateUser($user);
return true;
}
}
return false;
}
}
The programme record does get created in the database but with a null user_id so it seems the association isn't working correctly. Anyone know what might be causing this?
The solution was to do $programmes->setUser($this); in the addProgrammes method of my User entity