I'm trying to rename fields and update values on already inserted rows in cakephp migrations.
Workflow:
In before method i fetch all already added rows in my db so i will be able to update them in after method.
Then migration happens, which create columns value_from and value_to and also drops columns price_from and price_to
Then i try in my after method fetch all "new" rows, where i wan't to update values from old ones, but error happens, because find('all') method throws errors
My code:
class ShippingTypeCostCalculationM1483610977ProjectPluginLogistics extends CakeMigration {
/**
* Data which will be inserted in modified table shipping costs
* #var
*/
private $data;
/**
* Migration description
*
* #var string
* #access public
*/
public $description = '';
/**
* Actions to be performed
*
* #var array $migration
* #access public
*/
public $migration = array(
'up' => array(
'create_field' => array(
'shipping_costs' => array(
'shipping_type' => array('type' => 'string', 'null' => false, 'default' => 'order_sum', 'length' => 20, 'collate' => 'utf8_unicode_ci', 'charset' => 'utf8', 'after' => 'customer_type'),
'value_from' => array('type' => 'decimal', 'null' => false, 'default' => NULL, 'length' => '10,4', 'after' => 'shipping_type'),
'value_to' => array('type' => 'decimal', 'null' => false, 'default' => NULL, 'length' => '10,4', 'after' => 'value_from'),
),
),
'drop_field' => array(
'shipping_costs' => array('price_from', 'price_to',),
)
),
'down' => array(
'drop_field' => array(
'shipping_costs' => array('shipping_type', 'value_from', 'value_to',),
),
),
'create_field' => array(
'shipping_costs' => array(
'price_from' => array('type' => 'decimal', 'null' => false, 'default' => NULL, 'length' => '10,4'),
'price_to' => array('type' => 'decimal', 'null' => false, 'default' => NULL, 'length' => '10,4'),
),
)
);
/**
* Before migration callback
*
* #param string $direction, up or down direction of migration process
* #return boolean Should process continue
* #access public
*/
public function before($direction) {
$shippingCost = ClassRegistry::init('Logistics.ShippingCost');
$this->data = $shippingCost->find('all');
return true;
}
/**
* After migration callback
*
* #param string $direction, up or down direction of migration process
* #return boolean Should process continue
* #access public
*/
public function after($direction) {
$shippingCost = ClassRegistry::init('Logistics.ShippingCost');
// This is where error happens
$shippingCosts = $shippingCost->find('all');
foreach ($this->data as $item) {
$shippingCost = $shippingCosts->get($item['shipping_costs']['id']);
$shippingCost->value_from = $item['shipping_costs']['price_from'];
$shippingCost->value_to = $item['shipping_costs']['price_to'];
$shippingCosts->save($shippingCost);
}
return true;
}
}
PROBLEM:
In after method, cake php is still trying to fetch values like price_from or price_to which are already deleted.
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'ShippingCost.price_from' in 'field list'
How can i overcome my problem?
If you need any additional informations, please let me know and i will provide. Thank you in advance!
You run the callbacks in any case, the code is always executed.
public function after($direction) {...}
See the argument? This can be up and down. You need to wrap your code that you want to run before or after applying the migration accordingly in a check like if ($direction === 'down') { ... }.
Also I don't think your approach is well done. If you have a lot of data you can run out of memory.
Create a migration that will add the new fields. Then another migration file that will just do the data transformation and process the data in chunks. And a third one that will delete the no longer needed fields after that. The simple reason is: Separate data from schema changes.
Related
I have an Announcement Entity that where a EditAnnouncementType form is mapped to. I have two other CheckBoxType forms that automatically update their respective fields, but the ChoiceType form isn't working.
$builder
->add('edit', SubmitType::class,
array
(
'label' => 'Save changes',
'attr' => ['class' => 'btn btn-primary']
))
->add('type', ChoiceType::class,
[
'choices' => [
'info_type' => 1,
'star_type' => 2,
'alert_type' => 3,
'lightbulb_type' => 3,
'event_type' => 4,
'statement_type' => 5,
'cat_type' => 6,
'hands_type' => 7
],
'mapped' => true,
'expanded' => true,
'required' => true,
])
The Announcement entity and type field
class Announcement
{
/**
* #var int
*/
private $type;
/**
* #return string
*/
public function getType(): string
{
return $this->type;
}
/**
* #param int $type
*
* #return $this
*/
public function setType(int $type)
{
$this->type = $type;
return $this;
}
My suspicion would be that Symfony somehow strict checks the value (using ===).
And since your getter returns a string, the mapping doesn't happen properly.
You should try fixing your getter:
/**
* #return int
*/
public function getType(): int
{
return $this->type;
}
Also mind that you might have a problem in your choice array:
// be careful: those two have the same value
'alert_type' => 3,
'lightbulb_type' => 3,
This would surely cause an issue to Symfony, especially if it uses array_values in order to select the right choice out of the value of your entity.
I'm really confused about my Form Filter.
My Test-Project contains 2 Models.
class Category extends AbstractEntity
{
use Nameable; // just property name and getter and setter
/**
* #var boolean
* #ORM\Column(name="issue", type="boolean")
*/
private $issue;
/**
* #var Collection|ArrayCollection|Entry[]
*
* #ORM\OneToMany(targetEntity="CashJournal\Model\Entry", mappedBy="category", fetch="EAGER", orphanRemoval=true, cascade={"persist", "remove"})
*/
private $entries;
}
the entry
class Entry extends AbstractEntity
{
use Nameable;
/**
* #var null|float
*
* #ORM\Column(name="amount", type="decimal")
*/
private $amount;
/**
* #var null|Category
*
* #ORM\ManyToOne(targetEntity="CashJournal\Model\Category", inversedBy="entries", fetch="EAGER")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*/
protected $category;
/**
* #var null|DateTime
*
* #ORM\Column(name="date_of_entry", type="datetime")
*/
private $dateOfEntry;
}
And if someone needed the AbstractEntity
abstract class AbstractEntity implements EntityInterface
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
}
Every Category can have many Entries. I'm using Doctrine for this relation. And this works fine.
I have a Form based on this FieldSet:
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
]
]);
So my Form displays a dropdown with my categories. Yeah fine.
To load the Category for my Entry Entity i use a filter.
$this->add([
'name' => 'category',
'required' => true,
'filters' => [
[
'name' => Callback::class,
'options' => [
'callback' => [$this, 'loadCategory']
]
]
]
]);
And the callback:
public function loadCategory(string $categoryId)
{
return $this->mapper->find($categoryId);
}
The mapper loads the category fine. great. But the form is invalid because:
Object of class CashJournal\Model\Category could not be converted to int
Ok, so i'm removing the Filter, but now it failed to set the attributes to the Entry Entity, because the setter needs a Category. The Form error says:
The input is not a valid step
In Symfony i can create a ParamConverter, which converts the category_id to an valid Category Entity.
Question
How i can use the filter as my ParamConver?
Update
Also when i cast the category_id to int, i will get the error from the form.
Update 2
I changed my FieldSet to:
class EntryFieldSet extends Fieldset implements ObjectManagerAwareInterface
{
use ObjectManagerTrait;
/**
* {#inheritDoc}
*/
public function init()
{
$this->add([
'name' => 'id',
'type' => Hidden::class
]);
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name'
]
]);
$this->add([
'name' => 'amount',
'type' => Number::class,
'options' => [
'label' => 'Summe'
]
]);
$this->add([
'name' => 'date_of_entry',
'type' => Date::class,
'options' => [
'label' => 'Datum'
]
]);
$this->add([
'name' => 'category',
'required' => false,
'type' => ObjectSelect::class,
'options' => [
'target_class' => Category::class,
'object_manager' => $this->getObjectManager(),
'property' => 'id',
'display_empty_item' => true,
'empty_item_label' => '---',
'label_generator' => function ($targetEntity) {
return $targetEntity->getName();
},
]
]);
parent::init();
}
}
But this will be quit with the error message:
Entry::setDateOfEntry() must be an instance of DateTime, string given
Have you checked the documentation for ObjectSelect? You appear to be missing a few options, namely which hydrator (EntityManager) and identifying property (id) to use. Have a look here.
Example:
$this->add([
'type' => ObjectSelect::class,
'name' => 'category', // Name of property, 'category' in your question
'options' => [
'object_manager' => $this->getObjectManager(), // Make sure you provided the EntityManager to this Fieldset/Form
'target_class' => Category::class, // Entity to target
'property' => 'id', // Identifying property
],
]);
To validate selected Element, add in your InputFilter:
$this->add([
'name' => 'category',
'required' => true,
]);
No more is needed for the InputFilter. A Category already exist and as such has been validated before. So, you should just be able to select it.
You'd only need additional filters/validators if you have special requirements, for example: "A Category may only be used once in Entries", making it so that you need to use a NoObjectExists validator. But that does not seem to be the case here.
UPDATE BASED ON COMMENTS & PAST QUESTIONS
I think you're over complicating a lot of things in what you're trying to do. It seems you want to simply populate a Form before you load it client-side. On receiving a POST (from client) you wish to put the received data in the Form, validate it and store it. Correct?
Based on that, please find a complete controller for User that I have in one of my projects. Hope you find it helpful. Providing it because updates are veering away from your original question and this might help you out.
I've removed some additional checking and error throwing, but otherwise is in complete working fashion.
(Please note that I'm using my own abstract controller, make sure to replace it with your own and/or recreate and match requirements)
I've also placed additional comments throughout this code to help you out
<?php
namespace User\Controller\User;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\ORMException;
use Exception;
use Keet\Mvc\Controller\AbstractDoctrineActionController;
use User\Entity\User;
use User\Form\UserForm;
use Zend\Http\Request;
use Zend\Http\Response;
class EditController extends AbstractDoctrineActionController
{
/**
* #var UserForm
*/
protected $userEditForm; // Provide this
public function __construct(ObjectManager $objectManager, UserForm $userEditForm)
{
parent::__construct($objectManager); // Require this in this class or your own abstract class
$this->setUserEditForm($userEditForm);
}
/**
* #return array|Response
* #throws ORMException|Exception
*/
public function editAction()
{
$id = $this->params()->fromRoute('id', null);
// check if id set -> else error/redirect
/** #var User $entity */
$entity = $this->getObjectManager()->getRepository(User::class)->find($id);
// check if entity -> else error/redirect
/** #var UserForm $form */
$form = $this->getUserEditForm(); // GET THE FORM
$form->bind($entity); // Bind the Entity (object) on the Form
// Only go into the belof if() on POST, else return Form. Above the data is set on the Form, so good to go (pre-filled with existing data)
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost()); // Set received POST data on Form
if ($form->isValid()) { // Validates Form. This also updates the Entity (object) with the received POST data
/** #var User $user */
$user = $form->getObject(); // Gets updated Entity (User object)
$this->getObjectManager()->persist($user); // Persist it
try {
$this->getObjectManager()->flush(); // Store in DB
} catch (Exception $e) {
throw new Exception('Could not save. Error was thrown, details: ', $e->getMessage());
}
return $this->redirectToRoute('users/view', ['id' => $user->getId()]);
}
}
// Returns the Form with bound Entity (object).
// Print magically in view with `<?= $this->form($form) ?>` (prints whole Form!!!)
return [
'form' => $form,
];
}
/**
* #return UserForm
*/
public function getUserEditForm() : UserForm
{
return $this->userEditForm;
}
/**
* #param UserForm $userEditForm
*
* #return EditController
*/
public function setUserEditForm(UserForm $userEditForm) : EditController
{
$this->userEditForm = $userEditForm;
return $this;
}
}
Hope that helps...
In a MySQL database, I'm storing a VARBINARY(16) for an IPv4/IPv6 address. In order to display this on a form, I have created a DataTransformer in order to use the built-in inet_pton and inet_ntop functions.
I would like to display this in a Datatable, so searching by IP address would be possible. Unfortunately, by using my typical datatable setup, I get 500 server errors.
Through using browser developer tools, I'm able to determine that the results from the AJAX call is getting partial data, the actual IP address is listed as a resource, not the real data.
The error says: An unexpected value could not be normalized: NULL
In the stack trace, I see:
at Serializer ->normalize (resource, 'json', array())
in vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php at line 152
at Serializer ->normalize (array('id' => '5', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))), 'json', array())
in vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php at line 152
at Serializer ->normalize (array(array('id' => '5', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))), array('id' => '6', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true)))), 'json', array())
in vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php at line 152
at Serializer ->normalize (array('draw' => '1', 'recordsTotal' => '2', 'recordsFiltered' => '2', 'data' => array(array('id' => '5', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))), array('id' => '6', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))))), 'json', array())
in vendor/symfony/symfony/src/Symfony/Component/Serializer/Serializer.php at line 115
at Serializer ->serialize (array('draw' => '1', 'recordsTotal' => '2', 'recordsFiltered' => '2', 'data' => array(array('id' => '5', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))), array('id' => '6', 'ipAddress' => resource, 'sg_datatables_editable' => array(false), 'sg_datatables_actions' => array(array('network_ipaddress_show' => true))))), 'json')
in vendor/sg/datatablesbundle/Sg/DatatablesBundle/Datatable/Data/DatatableQuery.php at line 743
Seeing the JSON calls, I implemented the JsonSerializable interface, but it seems that it's not being pulled that way.
While an option would be to place the inet_pton or inet_ntop in the entity directly, I believe I would lose validation.
What interface can I implement to allow Datatables to pull my IPAddress entity, and display the IP address properly?
Note that the data isn't translated from the entity (VARBINARY) to a readable string that the datatable requires. The stack trace shows that it's a "resource".
Added Code:
My Entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
/**
* #ORM\Table(name="ip_address")
*/
class IPAddress implements JsonSerializable
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var mixed
* #ORM\Column(name="ip_address", type="binary", length=16, nullable=false, unique=true)
*/
private $ipAddress;
/**
* Set IP Address
* #param mixed $ipAddress
* #return IPAddress
*/
public function setIpAddress($ipAddress)
{
$this->ipAddress = $ipAddress;
return $this;
}
/**
* Get IP Address
* #return mixed
*/
public function getIpAddress()
{
return $this->ipAddress;
}
public function jsonSerialize()
{
return array(
'ipAddress' => inet_pton($this->ipAddress),
);
}
The Datatable class:
<?php
namespace AppBundle\Datatables;
use Sg\DatatablesBundle\Datatable\View\AbstractDatatableView;
use Sg\DatatablesBundle\Datatable\View\Style;
/**
* Class IPAddressDatatable
* #package AppBundle\Datatables
*/
class IPAddressDatatable extends AbstractDatatableView
{
/**
* {#inheritdoc}
*/
public function buildDatatable(array $options = array())
{
$this->features->set(array(
'auto_width' => true,
'defer_render' => false,
... other default features not listed ...
'delay' => 0,
'extensions' => array()
));
$this->ajax->set(array(
'url' => $this->router->generate('network_ipaddress_results'),
'type' => 'GET'
));
$this->options->set(array(
'display_start' => 0,
'defer_loading' => -1,
... other default options not listed ...
'use_integration_options' => false,
'force_dom' => false
));
$this->columnBuilder
->add('ipAddress', 'column', array(
'title' => 'IP Address',
'width' => '85%',
))
->add(null, 'action', array(
'title' => '',
'width' => '15%',
'actions' => array(
array(
'route' => 'network_ipaddress_show',
'route_parameters' => array(
'plan_id' => 'id'
),
'label' => $this->translator->trans('datatables.actions.show'),
'icon' => 'fi-eye icon-size-14',
'attributes' => array(
'rel' => 'tooltip',
'title' => $this->translator->trans('datatables.actions.show'),
'class' => 'tiny button',
'role' => 'button'
),
),
),
));
;
}
... getEntity and getName not shown for brevity ...
}
The Controller (at least the relevant parts):
/**
* Lists all IP Addresses.
*
* #Route("/", name="network_ipaddress_index")
* #Method("GET")
* #return IPAddressController
*/
public function indexAction()
{
$datatable = $this->get('app.datatable.ipaddress');
$datatable->buildDatatable();
return $this->render(':network:ipaddress_datatable.html.twig', array(
'datatable' => $datatable,
));
}
/**
* Returns a response, only used for datatables
*
* #Route("/results", name="network_ipaddress_results")
* #Method("GET")
* #return IPAddressController
*/
public function indexResultsAction()
{
$datatable = $this->get('app.datatable.ipaddress');
$datatable->buildDatatable();
// I believe that the entity data would have to be translated here into printable a format
$query = $this->get('sg_datatables.query')->getQueryFrom($datatable);
return $query->getResponse();
}
As it turns out, there is a closure available pre-rendering in the stwe/DatatablesBundle.
So, the function that is required in the above Datatables class:
/**
* {#inheritdoc}
*/
public function getLineFormatter()
{
$formatter = function($line){
$str = stream_get_contents($line['ipAddress']);
if( strlen( $str ) == 16 OR strlen( $str ) == 4 ){
$line['ipAddress'] = inet_ntop( pack( "A".strlen( $str ) , $str ) );
}
return $line;
};
return $formatter;
}
I'm trying to create a 'link_field' in Drupal through a module, I've got the following code but I'd like to configure the field settings to do the following:
Not require title
Update the number of values to unlimited
I'm struggling to find any information around what setting key/pair values you can pass through for these, anyone able to offer some guidance on these?
<?php
/**
* Implements hook_enable().
*
* Create a field. Fields can be created without any needs to attach them to
* entities.
*/
function youtube_carousel_enable() {
$field = array(
'field_name' => 'ytcarousel_field',
'type' => 'link_field',
);
field_create_field($field);
/**
* Bind field to a entity bundle.
*/
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => 'node',
'bundle' => 'homepage',
'label' => 'YouTube Video'
);
field_create_instance($instance);
}
/**
* Implements hook_disable().
*
* Remove field from node bundle (content type) and then delete the field.
*/
function youtube_carousel_disable() {
$instance = array(
'field_name' => 'ytcarousel_field',
'entity_type' => 'node',
'bundle' => 'homepage',
'label' => 'YouTube Video'
);
field_delete_instance($instance);
field_delete_field($instance['field_name']);
print 'Removed ' . $instance['field_name'] . "\n";
}
?>
Cheers
After viewing the HTML source to get key/value pairs I've managed to do something similar to the above with the following code:
<?php
/**
* Implements hook_enable().
*
* Create a field. Fields can be created without any needs to attach them to
* entities.
*/
function youtube_carousel_enable() {
$field = array(
'field_name' => 'ytcarousel_field',
'type' => 'link_field',
'cardinality' => -1
);
field_create_field($field);
/**
* Bind field to a entity bundle.
*/
$instance = array(
'field_name' => $field['field_name'],
'entity_type' => 'node',
'bundle' => 'homepage',
'label' => 'YouTube Video',
'settings' => array('title' => 'required')
);
field_create_instance($instance);
}
/**
* Implements hook_disable().
*
* Remove field from node bundle (content type) and then delete the field.
*/
function youtube_carousel_disable() {
$instance = array(
'field_name' => 'ytcarousel_field',
'entity_type' => 'node',
'bundle' => 'homepage',
'label' => 'YouTube Video',
'settings' => array('title' => 'required')
);
field_delete_instance($instance);
field_delete_field($instance['field_name']);
print 'Removed ' . $instance['field_name'] . "\n";
}
?>
The cardinality controls have many instances the value can have, "-1" is unlimited.
'cardinality' => -1
The title attribute is controlled via the instance not the field itself, I believe, and it uses text based values rather than numeric.
'settings' => array('title' => 'required')
I have 2 tables: options and optionselections.
Here are the models:
use LaravelBook\Ardent\Ardent;
class Option extends Ardent
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'options';
// MASS ASSIGNMENT -------------------------------------------------------
// define which attributes are mass assignable (for security)
// we only want these 1 attribute able to be filled
protected $fillable = array('name');
public function selections()
{
return $this->hasMany('optionselection');
}
}
use LaravelBook\Ardent\Ardent;
class Optionselection extends Ardent
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'option_selections';
// MASS ASSIGNMENT -------------------------------------------------------
// define which attributes are mass assignable (for security)
// we only want these 1 attribute able to be filled
protected $fillable = array('option_id', 'name');
public function choice()
{
$this->belongsTo('option');
}
}
I'm trying to create the relationship in Laravel administrator like I've done so many times before, and I can't see why I'm getting the error: The 'choice' relationship field you supplied for optionselections is not a valid relationship method name on the supplied Eloquent model
return array(
/**
* Model title
*
* #type string
*/
'title' => 'Option Selections',
/**
* The singular name of your model
*
* #type string
*/
'single' => 'Option Selection',
/**
* The class name of the Eloquent model that this config represents
*
* #type string
*/
'model' => 'optionselection',
/**
* The columns array
*
* #type array
*/
'columns' => array(
'choice' => array(
'title' => 'Option',
'relationship' => 'choice',
'select' => 'name',
),
'selection' => array(
'title' => 'Selection'
),
),
'edit_fields' => array(
'choice' => array(
'title' => 'Option',
'type' => 'relationship',
'name_field' => 'name',
),
'name' => array(
'title' => 'Selection Name',
'limit' => 30,
),
),
'action_permissions'=> array(
),
)
I know that the method/relationship field actually does exist and is recognized outside of Laravel Administrator, because this works:
$optsel = new Optionselection();
// var_dump($svcloc);
if (method_exists($optsel, "choice")) {
echo '<br/>Recognizes!';
} else {
echo '<br/>Problem!';
}
Why I'm getting the error?
Missing the return inside the relationship method. Closed