Add an auto_increment column in Magento setup script without using SQL - magento

Previously I asked how to ALTER TABLE in Magento setup script without using SQL. There, Ivan gave an excellent answer which I still refer to even now.
However I have yet to discover how to use Varien_Db_Ddl_Table::addColumn() to specify an auto_increment column. I think it has something to do with an option called identity but so far have had no luck.
Is this even possible or is that functionality incomplete?

One can create an autoincrement column like that (at least since Magento 1.6, maybe even earlier):
/** #var $table Varien_Db_Ddl_Table */
$table->addColumn( 'id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'auto_increment' => true,
'unsigned' => true,
'nullable' => false,
'primary' => true,
), 'ID' );
Instead of "auto_increment", one may also use the keyword "identity".

I think that's something that hasn't been implemented yet.
If you look at the source to addColumn, you can see it looks for a identity/auto_increment option and sets an IDENTITY attribute on the internal column representation.
#File: lib/Varien/Db/Ddl/Table.php
if (!empty($options['identity']) || !empty($options['auto_increment'])) {
$identity = true;
}
$upperName = strtoupper($name);
$this->_columns[$upperName] = array(
'COLUMN_NAME' => $name,
'COLUMN_TYPE' => $type,
'COLUMN_POSITION' => $position,
'DATA_TYPE' => $type,
'DEFAULT' => $default,
'NULLABLE' => $nullable,
'LENGTH' => $length,
'SCALE' => $scale,
'PRECISION' => $precision,
'UNSIGNED' => $unsigned,
'PRIMARY' => $primary,
'PRIMARY_POSITION' => $primaryPosition,
'IDENTITY' => $identity
);
However, if you look at the createTable method on the connection object
#File: lib/Varien/Db/Adapter/Pdo/Mysql.php
public function createTable(Varien_Db_Ddl_Table $table)
{
$sqlFragment = array_merge(
$this->_getColumnsDefinition($table),
$this->_getIndexesDefinition($table),
$this->_getForeignKeysDefinition($table)
);
$tableOptions = $this->_getOptionsDefination($table);
$sql = sprintf("CREATE TABLE %s (\n%s\n) %s",
$this->quoteIdentifier($table->getName()),
implode(",\n", $sqlFragment),
implode(" ", $tableOptions));
return $this->query($sql);
}
you can see _getColumnsDefinition, _getIndexesDefinition, and _getForeignKeysDefinition are used to create a CREATE SQL fragment. None of these methods make any reference to identity or auto_increment, nor do they appear to generate any sql that would create an auto increment.
The only possible candidates in this class are
/**
* Autoincrement for bind value
*
* #var int
*/
protected $_bindIncrement = 0;
which is used to control the increment number for a PDO bound parameter (nothing to do with auto_increment).
There's also a mention of auto_increment here
protected function _getOptionsDefination(Varien_Db_Ddl_Table $table)
{
$definition = array();
$tableProps = array(
'type' => 'ENGINE=%s',
'checksum' => 'CHECKSUM=%d',
'auto_increment' => 'AUTO_INCREMENT=%d',
'avg_row_length' => 'AVG_ROW_LENGTH=%d',
'comment' => 'COMMENT=\'%s\'',
'max_rows' => 'MAX_ROWS=%d',
'min_rows' => 'MIN_ROWS=%d',
'delay_key_write' => 'DELAY_KEY_WRITE=%d',
'row_format' => 'row_format=%s',
'charset' => 'charset=%s',
'collate' => 'COLLATE=%s'
);
foreach ($tableProps as $key => $mask) {
$v = $table->getOption($key);
if (!is_null($v)) {
$definition[] = sprintf($mask, $v);
}
}
return $definition;
}
but this is used to process options set on the table. This auto_increment controls the table AUTO_INCREMENT options, which can be used to control which integer an AUTO_INCREMENT starts at.

Related

ZF2 + Duplicate Form Validation on composite key

I have a Form having primary key on two fields (gid, bid). I need to add validation to block duplicate entries into database.
I have checked with ZF2 Solution for this . http://framework.zend.com/manual/2.2/en/modules/zend.validator.db.html#excluding-records . While this approach of handling composite keys is not look the ideal way, But still I am trying it because it look like only buil-in way. Now it require me to provide second field's value (value option in exclude), which is again a problem. As I am trying it
$inputFilter->add(array(
'name' => 'gid',
'required' => true,
'validators' => array(
array(
'name' => 'NotEmpty',
'options' => array(
'messages' => array(
'isEmpty' => 'required'
),
),
),
array (
'name' => 'Zend\Validator\Db\NoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
'exclude' => array(
'field' => 'bid',
'value' => [?],
),
)
),
)
));
How do I get this value, As Form is absolute separate Class/File than controller where I have the submitted form values. Is some better architecture solution of this problem exists Or Some hack to pass submitted field value to Form Class is only solution ?
Note : I am not in favor of Build My Validation Plugin for this task as short time is constraint for functionality.
You can do all the job in your form. To achieve that, you could define your forms as factories in your module Module.php.
Module.php
use MyNamespace\MyForm;
//NOTE THAT THE SERVICE MANAGER IS INJECTED. YOUR FORM COULD RECEIVE IT THROUGH THE CONSTRUCTOR
public function getServiceConfig()
{
return array(
'factories' => array(
'my_form' => function( $sm ) {
$form = new MyForm( $sm );
return $form;
},
),
);
}
When you want to use the form is as easy as use this code in your controller:
class MyController extends AbstractActionController
{
public function createAction() {
$form = $this->getServiceLocator()->get( 'my_form' ) );
(...)
}
}
And your MyForm.php
use Zend\Form\Form;
class MyForm extends Form
{
public $serviceManager, $request, $postData;
public function __construct( $serviceManager ) {
parent::__construct( null );
$this->serviceManager = $serviceManager;
$this->request = $serviceManager->get( 'Application')->getMvcEvent()->getRequest();
$this->postData = get_object_vars( $this->request->getPost() );
}
}
This way you can get advantage of the Service Manager within your form. And the public postData, where you'll find the bid value you're looking for to build your NoRecordExists filter.
You could add the parameters to the getInputFilter, like this :
getInputFilter($gid, $bid)
And then on the controller, when you set the filter you pass the 2 parameters, and then just check as $form->isValid(); ...
Alternative try this:
array(
'name' => 'Db\NoRecordExists',
'options' => array(
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
),
),
I'm unsure on your use case. If you were to add a database entry the primary keys for that table would not be known until you insert anyway - If you have foreign key constraints you could handle the exception from the database.
I am not in favor of Build My Validation Plugin for this task
The validator is also not designed to validate multiple fields as they are attached to a form element on a 1-1 basis. You will therefore need to create your own.
The below example has NOT been tested, so take it as an example of the approach rather than working code.
The key bit is the isValid method.
namespace MyModule\Validator\Db;
use Zend\Validator\Db\NoRecordExists;
class CompositeNoRecordExists extends NoRecordExists
{
protected $field2;
protected $field2Value;
public function __construct($options = null)
{
parent::__construct($options);
if (array_key_exists('field2', $options)) {
$this->setField2($options['field2']);
} else {
throw new \BadMethodCallException('Missing field2 option!');
}
}
protected function setField2Value(array $context)
{
if (! isset($context[$this->field2])) {
throw new \BadMethodCallException('Unable to find value for field 2');
}
$this->field2Value = $context[$this->field2];
}
public function isValid($value)
{
// The isValid() method is actually given a 2nd argument called $context
// Which is injected by the inputFilter, via the input and into the validator chain
// $context contains all of RAW form element values, keyed by thier element name.
// Unfortunately due to the ValidatorInterface you are unable to add this to the method
// signature. So you will need to be 'creative':
$args = func_get_args();
if (isset($args[1]) && is_array($args[1])) {
$this->setField2Value($args[1]);
} else {
throw new \BadMethodCallException('Missing validator context');
}
return parent::isValid($value);
}
public function getSelect()
{
$select = parent::getSelect();
$select->where->equalTo($this->field2, $this->field2Value);
return $select;
}
}
Then all you would need to do is update the validator config, adding the field2 field name.
array (
'name' => 'MyModule\Validator\Db\CompositeNoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'field2' => 'bid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
)
),

How to add categories in Joomla 2.5

Does anyone know how to properly insert new content categories to the DB programatically?
For each post in the categories table, there is also a post saved in the assets table with lft and rgt set.
Is there any native Joomla class I can use for this instead of plain SQL?
Please Please Only use the native classes, which categories will handle for you seamlessly. As soon as you add categories the whole thing will be handled automagically. Just look at any core component to see how.
It is not easy to update the assets table using sql, it is all very specifically managed and part of a complex series of foreign keyed tables.
Extend JTable or JTableContent to handle this.
Here is some code I just whipped together that just uses the JTableCategory class, so it can be used simply on the front or admin side of Joomla
$table = JTable::getInstance('category');
$data = array();
// name the category
$data['title'] = $title;
// set the parent category for the new category
$data['parent_id'] = $parent_id;
// set what extension the category is for
$data['extension'] = $extension;
// Set the category to be published by default
$data['published'] = 1;
// setLocation uses the parent_id and updates the nesting columns correctly
$table->setLocation($data['parent_id'], 'last-child');
// push our data into the table object
$table->bind($data);
// some data checks including setting the alias based on the name
if ($table->check()) {
// and store it!
$table->store();
// Success
} else {
// Error
}
Naturally you would want to get the data pieces set correctly, but these are the core ones to set.
Here is a function I've created just for this purpose, after some digging & experimenting.
It uses core classes, so it needs an access to them (for me it's basically a part of Joomla component).
Mind, it's for Joomla 3, for Joomla 2.5 and before, you need to change JModelLegacy to JModel.
function createCategory( $name, $parent_id, $note )
{
JTable::addIncludePath( JPATH_ADMINISTRATOR . '/components/com_categories/tables' );
$cat_model = JModelLegacy::getInstance( 'Category', 'CategoriesModel' );
$data = array (
'id' => 0,
'parent_id' => $parent_id,
'extension' => 'com_content',
'title' => $name,
'alias' => '',
'note' => $note,
'description' => '',
'published' => '1',
'access' => '1',
'metadesc' => '',
'metakey' => '',
'created_user_id' => '0',
'language' => '*',
'rules' => array(
'core.create' => array(),
'core.delete' => array(),
'core.edit' => array(),
'core.edit.state' => array(),
'core.edit.own' => array(),
),
'params' => array(
'category_layout' => '',
'image' => '',
),
'metadata' => array(
'author' => '',
'robots' => '',
),
);
if( !$cat_model->save( $data ) )
{
return NULL;
}
$categories = JCategories::getInstance( 'Content' );
$subcategory = $categories->get( $cat_model->getState( "category.id" ) );
return $subcategory;
}
You can perhaps use the save() in category.php file.
File location: root\administrator\components\com_categories\models\category.php
It saves the form data supplied to it!
The JOS_assets table is to store the ACL for each asset that is created.
If you do not update this table while programatically creating the category, the default ACL will apply. And when later you open and save the category in the administrative panel, the ACL will be updated as it should have been by core Joomla!.
You can create an SQL query very easily though and update the asset table as well. Its easy to understand once you open the table's content in phpmyadmin.

Magento Shipping method stops working after ALTER tables 'sales_flat_quote_address' and 'sales_flat_order_address'

I had ALTER tables 'sales_flat_quote_address' and 'sales_flat_order_address' to add a new column in order to add a custom field in Billing and Shipping process. But after doing that even the default shipping method stops working and it is not still not working after drop of this column. Below is the code I had used in my sql file of my module.
$installer = $this;
$installer->startSetup();
/* #var $addressHelper Mage_Customer_Helper_Address */
$addressHelper = Mage::helper('customer/address');
$store = Mage::app()->getStore(Mage_Core_Model_App::ADMIN_STORE_ID);
/* #var $eavConfig Mage_Eav_Model_Config */
$eavConfig = Mage::getSingleton('eav/config');
// update customer address user defined attributes data
$attributes = array(
'suburb' => array(
'label' => 'Suburb',
'type' => 'varchar',
'input' => 'text',
'is_user_defined' => 1,
'is_system' => 0,
'is_visible' => 1,
'sort_order' => 140,
'is_required' => 1,
'multiline_count' => 0,
'validate_rules' => array(
'max_text_length' => 255,
'min_text_length' => 1
),
),
);
foreach ($attributes as $attributeCode => $data) {
$attribute = $eavConfig->getAttribute('customer_address', $attributeCode);
$attribute->setWebsite($store->getWebsite());
$attribute->addData($data);
$usedInForms = array(
'adminhtml_customer_address',
'customer_address_edit',
'customer_register_address'
);
$attribute->setData('used_in_forms', $usedInForms);
$attribute->save();
}
$installer->run("
ALTER TABLE {$installer->getTable('sales_flat_quote_address')} ADD COLUMN suburb VARCHAR(255) CHARACTER SET utf8 DEFAULT NULL AFTER fax;
ALTER TABLE {$installer->getTable('sales_flat_order_address')} ADD COLUMN suburb VARCHAR(255) CHARACTER SET utf8 DEFAULT NULL AFTER fax;
");
$installer->endSetup();
by fiddling with shipping options I noticed it may stop checkout (not loading shipping methods) but it thows fatal error. Enable logging in your backend, then navigate to your checkout and try selecting everything up to non-working shipping method. when this is done, check your exception log, and var/reports folder. these may give you some information what has gone wrong in your particular case.

Magento - How to create "decimal" attribute type

I've done a bit of searching online but I have not found any answers to this question yet. I have a situation where I need a product attribute that is a decimal value and it must support negative numbers as well as positive and must also be sortable. For some reason, Magento does not have a "decimal" attribute type. The only type that uses decimal values is Price, but that doesn't support negative numbers. If I use "text" as the type, it supports whatever I want, but it doesn't sort properly because it sees the values as strings rather than floating point numbers. I have been able to work around this issue, as others have in posts I've found, by manually editing the eav_attribute table and changing 'frontend_input' from 'price' to 'text', but leaving the 'backend_type' as 'decimal'. This works great...until someone edits the attribute in the admin panel. Once you save the attribute, Magento notices that the frontend_input is 'text' and changes the 'backend_type' to 'varchar'. The only way around this that I can think of is by creating a custom attribute type, but I'm not sure where to start and I can't find any details online for this.
Has anyone else experienced this problem? If so, what have you done to correct it? If I need to create a custom attribute type, do you have any tips or can you point me at any tutorials out there for doing this?
Thanks!
What you want to do is create a custom attribute type.
This can be done by first creating a installer script (this updates the database).
startSetup();
$installer->addAttribute('catalog_product', 'product_type', array(
'group' => 'Product Options',
'label' => 'Product Type',
'note' => '',
'type' => 'dec', //backend_type
'input' => 'select', //frontend_input
'frontend_class' => '',
'source' => 'sourcetype/attribute_source_type',
'backend' => '',
'frontend' => '',
'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_WEBSITE,
'required' => true,
'visible_on_front' => false,
'apply_to' => 'simple',
'is_configurable' => false,
'used_in_product_listing' => false,
'sort_order' => 5,
));
$installer->endSetup();
After that you need to create a custom php class named:
Whatever_Sourcetype_Model_Attribute_Source_Type
And in there paste this in:
class Whatever_Sourcetype_Model_Attribute_Source_Type extends Mage_Eav_Model_Entity_Attribute_Source_Abstract
{
const MAIN = 1;
const OTHER = 2;
public function getAllOptions()
{
if (is_null($this->_options)) {
$this->_options = array(
array(
'label' => Mage::helper('sourcetype')->__('Main Product'),
'value' => self::MAIN
),
array(
'label' => Mage::helper('sourcetype')->__('Other Product'),
'value' => self::OTHER
),
);
}
return $this->_options;
}
public function toOptionArray()
{
return $this->getAllOptions();
}
public function addValueSortToCollection($collection, $dir = 'asc')
{
$adminStore = Mage_Core_Model_App::ADMIN_STORE_ID;
$valueTable1 = $this->getAttribute()->getAttributeCode() . '_t1';
$valueTable2 = $this->getAttribute()->getAttributeCode() . '_t2';
$collection->getSelect()->joinLeft(
array($valueTable1 => $this->getAttribute()->getBackend()->getTable()),
"`e`.`entity_id`=`{$valueTable1}`.`entity_id`"
. " AND `{$valueTable1}`.`attribute_id`='{$this->getAttribute()->getId()}'"
. " AND `{$valueTable1}`.`store_id`='{$adminStore}'",
array()
);
if ($collection->getStoreId() != $adminStore) {
$collection->getSelect()->joinLeft(
array($valueTable2 => $this->getAttribute()->getBackend()->getTable()),
"`e`.`entity_id`=`{$valueTable2}`.`entity_id`"
. " AND `{$valueTable2}`.`attribute_id`='{$this->getAttribute()->getId()}'"
. " AND `{$valueTable2}`.`store_id`='{$collection->getStoreId()}'",
array()
);
$valueExpr = new Zend_Db_Expr("IF(`{$valueTable2}`.`value_id`>0, `{$valueTable2}`.`value`, `{$valueTable1}`.`value`)");
} else {
$valueExpr = new Zend_Db_Expr("`{$valueTable1}`.`value`");
}
$collection->getSelect()
->order($valueExpr, $dir);
return $this;
}
public function getFlatColums()
{
$columns = array(
$this->getAttribute()->getAttributeCode() => array(
'type' => 'int',
'unsigned' => false,
'is_null' => true,
'default' => null,
'extra' => null
)
);
return $columns;
}
public function getFlatUpdateSelect($store)
{
return Mage::getResourceModel('eav/entity_attribute')
->getFlatUpdateSelect($this->getAttribute(), $store);
}
}
Hope this helps.
For further info see here.

Remove column in postUp() method

Is possible to remove a column in a migration postUp() method or is only intended to be used for data manipulation?
I don't know what version of Doctrine Migrations you are using. However, I just ran across this same question when working with Doctrine Migrations 2.0 and started digging into the code. When looking at how a Version is constructed, it appears that using the Schema object to make changes in the postUp() method does not take effect.
However, upon further consideration, this really makes sense. Each migration version is intended to modify the database structure. This takes place in the up() and down() methods of each migration. postUp() seems to be intended mostly for post structural changes cleanup (i.e. manipulating data). Any further structural modifications that you had intended on making in the postUp() method are should be made in a subsequent migration file.
For example, I was attempting to create a new table that was going to hold two columns from a previous table. I intended to drop those columns from the previous table after I had migrated data over to the new table. The code follows:
class Version20110512223208 extends AbstractMigration
{
protected $customerRepository;
protected $evernoteRepository;
public function up(Schema $schema)
{
$table = $schema->createTable('customer_evernote');
$table->addOption('type', 'INNODB');
$table->addOption('charset', 'utf8');
$table->addOption('collate', 'utf8_unicode_ci');
// Columns.
$table->addColumn('customer_id', 'bigint', array(
'length' => 20,
'notnull' => true,
'autoincrement' => false));
$table->addColumn('integration_date', 'datetime', array('notnull' => true));
$table->addColumn('oauth_token', 'string', array(
'length' => 255,
'notnull' => true));
$table->addColumn('oauth_shard_id', 'string', array(
'length' => 4,
'notnull' => true,
'fixed' => true));
$table->setPrimaryKey(array('customer_id'), 'pk_customer_id');
$table->addForeignKeyConstraint($schema->getTable('customer'), array('customer_id'), array('id'));
}
public function down(Schema $schema)
{
$schema->dropTable('customer_evernote');
}
public function preUp(Schema $schema)
{
$this->addSql("ALTER TABLE `customer` ENGINE = INNODB");
}
public function postUp(Schema $schema)
{
$this->skipIf($this->version->isMigrated() !== true, 'postUp can only apply if migration completes.');
// Copy the data from the customer table into the newly created customer_evernote table.
$this->doctrine = \Zend_Registry::get('doctrine');
$this->entityManager = $this->doctrine->getEntityManager();
$this->customerRepository = $this->entityManager->getRepository('My\Entity\Customer');
$this->evernoteRepository = $this->entityManager->getRepository('My\Entity\CustomerEvernote');
$customers = $this->customerRepository->findAll();
foreach ($customers as $customer)
{
$evernoteRecord = new \My\Entity\CustomerEvernote();
$evernoteRecord->setCustomerId($customer->getId());
$evernoteRecord->setCustomer($customer);
$evernoteRecord->setOauthToken($customer->getEvernoteOauthToken());
$evernoteRecord->setOauthShardId($customer->getEvernoteOauthShardId());
$evernoteRecord->setIntegrationDate(new \DateTime("now"));
$this->evernoteRepository->saveEvernote($evernoteRecord);
}
// Drop the columns from the existing customer table.
$table = $schema->getTable('customer');
$table->dropColumn('evernote_oauth_token');
$table->dropColumn('evernote_oauth_shard_id');
}
public function preDown(Schema $schema)
{
// Create the existing columns in the customer table.
$table = $schema->getTable('customer');
$table->addColumn('evernote_oauth_token', 'string', array(
'length' => 255,
'notnull' => false));
$table->addColumn('evernote_oauth_shard_id', 'string', array(
'length' => 4,
'notnull' => false,
'fixed' => true));
// Copy the data to the customer table.
$this->doctrine = \Zend_Registry::get('doctrine');
$this->entityManager = $this->doctrine->getEntityManager();
$this->customerRepository = $this->entityManager->getRepository('My\Entity\Customer');
$this->evernoteRepository = $this->entityManager->getRepository('My\Entity\CustomerEvernote');
$integrations = $this->evernoteRepository->findAll();
foreach ($integrations as $integration)
{
$integration->getCustomer()->setEvernoteOauthToken($integration->getOauthToken());
$integration->getCustomer()->setEvernoteOauthShardId($integration->getOauthShardId());
$this->customerRepository->saveCustomer($integration->getCustomer());
}
}
}
In reality, if I were to move the code at the end of postUp() to a new version's up() and the code at the beginning of the preDown() to a new version's down() method, I get the same results as in the class above, just performed in two separate steps. With this approach, I guarantee that I have structural modifications strictly taking place in my up and down methods.

Resources