if you ever encountered a problem, when you cannot issue a sluggable behavior on a translated field, I feel ya.
Whenever you save a translation for an entity, 'slug' property is omitted because it's not dirty in the time of saving the translation entity.
You save an entity.
Translations are being created.
The table for i18n has no sluggable behavior attached, so it does not know, when to issue a sluggable behavior on a translated field like title / name etc.
I think I've found a better solution:
In my SluggableBehavior class, I've updated the behavior to include translations too:
public function beforeSave(Event $event, EntityInterface $entity) {
$this->slug($entity);
if($entity->get('_translations')) {
foreach($entity->get('_translations') as $key=>$translation) {
$this->slug($translation);
}
}
}
Of course, simply as it can be, it does not need a separate table :-) But thanks #ndm.
You can create and use a concrete table class for the translation table, where you can then create the slugs.
By default the name that the translate behavior uses for looking up table classes is I18n, so if you want this to apply to all translated tables, create App\Model\Table\I18nTable, or if you want this to apply to specific translated tables only, create a separate database translation table and class, and configure the translate behavior accordingly via the translationTable option:
// looks up `App\Model\Table\CustomI18nTable`
'translationTable' => 'CustomI18n'
See also
Cookbook > Database Access & ORM > Behaviors > Translate > Using a Separate Translations Table
Solution I think of might be and it's tested:
you specify a protected property in your entity, like:
protected $_sluggable = 'title';
then you create a getter:
public function _getSluggableField() {
return $this->_sluggable;
}
as soon as you do that you need to update the vendor file:
vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php
and change:
foreach ($translations as $lang => $translation) {
foreach ($fields as $field) {
if (!$translation->isDirty($field)) {
continue;
}
$find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key];
$contents[] = new Entity(['content' => $translation->get($field)], [
'useSetters' => false
]);
}
}
to:
foreach ($translations as $lang => $translation) {
foreach ($fields as $field) {
if($field==='slug' && (method_exists($entity, '_getSluggableField') && $entity->_getSluggableField())) {
$translation->set('slug', \Cake\Utility\Text::slug($translation->get($entity->_getSluggableField())));
}
if (!$translation->isDirty($field)) {
continue;
}
$find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key];
$contents[] = new Entity(['content' => $translation->get($field)], [
'useSetters' => false
]);
}
}
I hope someone has a better solution. But this one works as a charm.
Related
I'm creating a plugin FileManager where all uploads are stored in a single table. This plugin has an AttachmentBehavior that attaches a hasMany association.
I use multi-files input in templates Articles/add.php and Articles/edit.php to upload files that will be linked to an article :
// Example in Articles/edit.php
echo $this->Form->create($article, ['type' => 'file']);
echo $this->Form->control('title', /*[...]*/);
echo $this->Form->control('body', /*[...]*/);
echo $this->Form->control('pieces_jointes', ['type' => 'file', 'multiple' => true, 'name' => 'pieces_jointes[]']);
I can add new article with files, there's no problem.
I can edit an article that doesn't not have file to add files, there's no problem.
But when I edit an article that already have files to add some more files, I have an error "Cannot use object of type Laminas\Diactoros\UploadedFile as array"
This error appears when the entity Article is patched.
Here's my controller :
// in ArticlesController.php
public function edit($id)
{
$article = $this->Articles->findById($id)->firstOrFail();
if ($this->request->is(['post', 'put'])) {
debug($article); // $article->pieces_jointes is an array of entities of my files table.
debug($this->request->getData()); // $this->request->getData()->pieces_jointes is an array of UplaodedFile objects
$article = $this->Articles->patchEntity($article, $this->request->getData()); // The error occurs here
if ($this->Articles->save($article)) {
return $this->redirect(/*[...]*/);
}
}
$this->set(compact('item'));
}
It's not really clear for me about what is going on.
Does anyone can explain me and help me to solve this problem ?
You shouldn't use the same name for the upload field and the association property, that's a clash waiting to happen in various places of the framework.
Rename the fields in your forms, etc, so that it's using a name that neither matches any association property, nor any column name, and then let your behavior and stuff process the input using the "external" name, and convert it to the "internal" name after transforming the input into the structure required for saving the data.
Removing associated file Model in AttachmentBehavior::beforeMarshal seems to fix the Error :
// in AttachmentBehavior
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
foreach ($this->_fields as $field => $value) {
// Remove associated file Model (PiecesJointes, ...) in $options['associated']
$options['associated'] = collection($options['associated'])
->reject(function ($modelAssociated, $key) use ($value) {
return $modelAssociated == $value['alias'];
})
->toArray();
// [...]
}
}
But I would need the confirmation that I'm right (?)
I am very much new to laravel framework.
I have one form , which i need to update on submit button click.
when submit button clicks control goes to controller.php 's update() function .
But I am unable to edit any field's value.
here is my code.
public function update($id)
{
//echo "<pre>";print_r(Input::all());exit;
$product = $this->product->find($id);
$input = Input::only('designer', 'sku', 'name', 'display_name', 'description', 'price', 'main_category', 'sub_category', 'lead_time', 'sizing', 'woven', 'body_fabric', 'lining_fabric', 'fit', 'primary_color', 'secondary_color', 'care_label', 'neck_type', 'closure', 'trims', 'special_finishings', 'image1', 'image2', 'image3', 'image4', 'image5','top', 'combo_products', 'keywords', 'visibility', 'featured');
//echo "<pre>";print_r($input);exit;
try
{
$this->adminNewProductForm->validate($input);
} catch(\Laracasts\Validation\FormValidationException $e)
{
return Redirect::back()->withInput()->withErrors($e->getErrors());
}
$slug = Str::slug(Input::get('name'));
$slug = $this->product->getSlug($slug);
$input = array_add($input, 'slug', $slug);
DB::transaction(function() use($product, $input)
{
$product->fill($input)->save();
$stock_count = 0;
if(!empty(Input::get('xsmall_size')))
{
$rows = DB::table('products_variants')->where('product_id', $product->id)->where('variant_name', 'XS')->get();
$stock_count += Input::get('xsmall_stock');
if(!empty($rows))
{
DB::table('products_variants')->where('product_id', $product->id)->where('variant_name', 'XS')->update(array('variant_specs' => Input::get('xsmall_size'), 'price_change' => Input::get('xsmall_price'), 'total_stock' => Input::get('xsmall_stock'), 'stock_used' => 0));
} else {
DB::table('products_variants')->insert(array('product_id' => $product->id, 'variant_name' => 'XS', 'variant_specs' => Input::get('xsmall_size'), 'price_change' => Input::get('xsmall_price'), 'total_stock' => Input::get('xsmall_stock'), 'stock_used' => 0));
}
}
$input = array();
$input['flagship_status'] = Input::get('flagship_status');
if(Input::get('flagship_status'))
{
$input['stock_count'] = Input::get('small_stock');
}else {
$input['stock_count'] = $stock_count;
}
$product->fill($input)->save();
});
//echo "<pre>";print_r(Input::all());exit;
return Redirect::back()->withFlashMessage('Product Updated Successfully!');
}
Also I cant understand , what is going on by this line ? because i did not find validate function anywhere in my code.
$this->adminNewProductForm->validate($input);
I need to update table products not products_variants.
validate is inherited from the FormRequst class.
https://laravel.com/api/5.0/Illuminate/Foundation/Http/FormRequest.html#method_validate
You've provided too much code and too little information. You said you need to update a specific table, but yet there are two lines where you are very intentionally manually updating a database entry.
This is one of them:
DB::table('products_variants')->where('product_id', $product->id)->where('variant_name', 'XS')->update(array('variant_specs' => Input::get('xsmall_size'), 'price_change' => Input::get('xsmall_price'), 'total_stock' => Input::get('xsmall_stock'), 'stock_used' => 0));
When you call this:
$product->fill($input)->save();
It also saves 'dirty' (modified) models that also belong to it, which can include products_variants relationships. From the sound of it, you are incorrectly applying changes directly through SQL, and then the model's save method is overwriting it.
You seem unclear about what your code is actually doing, and I would strongly suggest simplifying it down and adding in code as you begin to understand what each line does. I think your question is the byproduct of copying an example and adding your own work without understanding how Laravel handles relationships and models. There is almost never a good reason to use raw SQL or DB statements.
Pago, model exists in Yii. As I create relationships automatically?..
Example:
$pago = new Pago();
Now I want to show information:
echo $pago->iDTIPOTRAMITE->id;
It is assumed that the information must be loaded by default
thanks.
I think I found the solution here: model init()
With this method, set initial property values.
Example. Pago model.
public function init()
{
$documentos = DocumentacionT::model()->findAll(array(
'condition' => 'ID_TIPOSOLICITUD = :idTipo',
'params' => array(':idTipo' => 5)
));
$pagoDocumento = [];
foreach($documentos as $documento)
{
$pagoDocumento[] = new PagoDocumento();
}
$this->pagoDocumentos = $pagoDocumento;
}
I'm converting a database from being managed by SQL dumps to being managed by schemas and migrations. Part of this is seeding data. I've based what I'm doing from the schema example on CakePhp's page about Schemas.
The weird thing is that the first table to be seeded with data works without problem, and the second fails with an error like Table users for model User was not found in datasource default. This happens even if I change which table will be seeded: the first one succeeds (and I've checked in the database that the data is there) and the next one to be seeded fails.
I've also checked the error message against the database, and every table it complains about not existing does actually exist.
My 'schema.php' looks like this:
class AppSchema extends CakeSchema {
public function before($event = array()) {
return true;
}
private function create_many($class_name, $entries){
App::uses('ClassRegistry', 'Utility');
$class = ClassRegistry::init($class_name);
foreach($entries as $entry){
$class->create();
$class->save(array($class_name => $entry));
}
}
private function create_many_kv($class_name, $keys, $values_matrix){
$entries = array();
foreach($values_matrix as $values){
array_push($entries, array_combine($keys, $values));
}
$this->create_many($class_name, $entries);
}
public function after($event = array()) {
if (isset($event['create'])) {
switch ($event['create']) {
case 'users':
$this->create_many('User', array(
array('emailaddress' => 'email',
'password' => 'hash',
'role_id' => 1
),
array('emailaddress' => 'email2',
'password' => 'hash',
'role_id' => 3)
));
break;
case 'other_table':
$this->create_many('OtherTable', array(
array('id' => 1,
'name' => 'datum'),
array('id' => 2,
'name' => 'datum2')
));
break;
etc.
The answer for me here was to populate all of the tables after the last table has been created.
My best hypothesis for why this didn't work as described in the question is that Cake is caching the database structure in memory, and this isn't updated when the new tables are added. I can't find any documentation about clearing that structure cache so a workaround is the closest thing to a solution for now.
When inserting data to more than one table you’ll need to flush the database cache after each table is created. Cache can be disabled by setting $db->cacheSources = false in the before action().
public $connection = 'default';
public function before($event = array()) {
$db = ConnectionManager::getDataSource($this->connection);
$db->cacheSources = false;
return true;
}
As mentioned here I'm building a custom hydration strategy to handle my related objects in a select box in a form.
My form looks like this:
$builder = new AnnotationBuilder($entityManager);
$form = $builder->createForm(new MyEntity());
$form->add(new MyFieldSet());
$hydrator = new ClassMethodsHydrator();
$hydrator->addStrategy('my_attribute', new MyHydrationStrategy());
$form->setHydrator($hydrator);
$form->get('my_attribute')->setValueOptions(
$entityManager->getRepository('SecEntity\Entity\SecEntity')->fetchAllAsArray()
);
When I add a new MyEntity via the addAction everything works great.
I wrote fetchAllAsArray() to populate my selectbox. It lives within my SecEntityRepository:
public function fetchAllAsArray() {
$objects = $this->createQueryBuilder('s')
->add('select', 's.id, s.name')
->add('orderBy', 's.name ASC')
->getQuery()
->getResult();
$list = array();
foreach($objects as $obj) {
$list[$obj['id']] = $obj['name'];
}
return $list;
}
But in the edit-case the extract() function doesn't work. I'm not at the point where I see something of hydrate() so I'll leave it out for now.
My hydrator strategy looks like this:
class MyHydrationStrategy extends DefaultStrategy
{
public function extract($value) {
print_r($value);
$result = array();
foreach ($value as $instance) {
print_r($instance);
$result[] = $instance->getId();
}
return $result;
}
public function hydrate($value) {
...
}
The problem is as follows:
Fatal error: Call to a member function getId() on a non-object
The print_r($value) returns loads of stuff beginning with
DoctrineORMModule\Proxy__CG__\SecEntity\Entity\SecEntity Object
following with something about BasicEntityPersister and somewhere in the mess are my referenced entities.
The print_r($instance) prints nothing. It's just empty. Therefore I guess is the error message legit... but why can't I iterate over these objects?
Any ideas?
Edit:
Regarding to #Sam:
My attribute in the entity:
/**
* #ORM\ManyToOne(targetEntity="Path/To/Entity", inversedBy="whatever")
* #ORM\JoinColumn(name="attribute_id", referencedColumnName="id")
* #Form\Attributes({"type":"hidden"})
*
*/
protected $attribute;
My new selectbox:
$form->add(array(
'name' => 'attribute',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'MyLabel',
'object_manager' => $entityManager,
'target_class' => 'Path/To/Entity',
'property' => 'name'
)
));
My final hope is that I'm doing something wrong within the controller. Neither my selectbox is preselected nor the value is saved...
...
$obj= $this->getEntityManager()->find('Path/To/Entity', $id);
$builder = new \MyEnity\MyFormBuilder();
$form = $builder->newForm($this->getEntityManager());
$form->setBindOnValidate(false);
$form->bind($obj);
$form->setData($obj->getArrayCopy());
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$form->bindValues();
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('entity');
}
}
I still haven't come around to write the tutorial for that :S
I don't know if this is working with the annotationbuilder though! As the DoctrineModule\Form\Element\ObjectSelect needs the EntityManager to work. The options for the ObjectSelect are as follows:
$this->add(array(
'name' => 'formElementName',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'attributes' => array(
'required' => true
),
'options' => array(
'label' => 'formElementLabel',
'empty_option' => '--- choose formElementName ---',
'object_manager' => $this->getEntityManager(),
'target_class' => 'Mynamespace\Entity\Entityname',
'property' => 'nameOfEntityPropertyAsSelect'
)
));
In this case i make use of $this->getEntityManager(). I set up this dependency when calling the form from the ServiceManager. Personally i always do this from FactoryClasses. My FormFactory looks like this:
public function createService(ServiceLocatorInterface $serviceLocator)
{
$em = $serviceLocator->get('Doctrine\ORM\EntityManager');
$form = new ErgebnishaushaltProduktForm('ergebnisform', array(
'entity_manager' => $em
));
$classMethodsHydrator = new ClassMethodsHydrator(false);
// Wir fügen zwei Strategien, um benutzerdefinierte Logik während Extrakt auszuführen
$classMethodsHydrator->addStrategy('produktBereich', new Strategy\ProduktbereichStrategy())
->addStrategy('produktGruppe', new Strategy\ProduktgruppeStrategy());
$hydrator = new DoctrineEntity($em, $classMethodsHydrator);
$form->setHydrator($hydrator)
->setObject(new ErgebnishaushaltProdukt())
->setInputFilter(new ErgebnishaushaltProduktFilter())
->setAttribute('method', 'post');
return $form;
}
And this is where all the magic is happening. Magic, that is also relevant to your other Thread here on SO. First, i grab the EntityManager. Then i create my form, and inject the dependency for the EntityManager. I do this using my own Form, you may write and use a Setter-Function to inject the EntityManager.
Next i create a ClassMethodsHydrator and add two HydrationStrategies to it. Personally i need to apply those strategies for each ObjectSelect-Element. You may not have to do this on your side. Try to see if it is working without it first!
After that, i create the DoctrineEntity-Hydrator, inject the EntityManager as well as my custom ClassMethodsHydrator. This way the Strategies will be added easily.
The rest should be quite self-explanatory (despite the german classnames :D)
Why the need for strategies
Imo, this is something missing from the DoctrineEntity currently, but things are still in an early stage. And once DoctrineModule-Issue#106 will be live, things will change again, probably making it more comfortable.
A Strategy looks like this:
<?php
namespace Haushaltportal\Stdlib\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class ProduktbereichStrategy implements StrategyInterface
{
public function extract($value)
{
if (is_numeric($value) || $value === null) {
return $value;
}
return $value->getId();
}
public function hydrate($value)
{
return $value;
}
}
So whenever the $value is not numeric or null, meaning: it should be an Object, we will call the getId() function. Personally i think it's a good idea to give each Element it's own strategy, but if you are sure you won't be needing to change the strategy at a later point, you could create a global Strategy for several elements like DefaultGetIdStrategy or something.
All this is basically the good work of Michael Gallego aka Bakura! In case you drop by the IRC, just hug him once ;)
Edit An additional resource with a look into the future - updated hydrator-docs for a very likely, soon to be included, pull request