How do i execute SQL statements from an SQL file in Scheduler in TYPO3? - sql

The concept:
I have a task which imports some data on the database. The database schema can be changed at any time since it is exported to a folder. So i have to read the database file and create the structure. I would like to do it with TYPO3 API. Before TYPO3 9 i would do something like that:
$sqlQueries = explode(';', file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlQueries as $sqlQuery) {
$sqlQuery = trim($sqlQuery);
if (!empty($sqlQuery) && $this->db instanceof DatabaseConnection && method_exists($this->db, 'sql_query')) {
$this->db->sql_query($sqlQuery);
}
}
How do i do that with TYPO3 10?
Best regards

TYPO3 10 has already such functionality and you can find it in the maintenance module unter the
Analyze Database Structure. Thanks to Mathias Brodala i found my solution. (It can always be improved).
In your task you call the SqlReader class in order to extract the statements from the SQL file. The use namespace is the following (As of TYPO3 10)
use TYPO3\CMS\Core\Database\Schema\SqlReader;
Now we have two problems with that
The SQL evaluated and executed by TYPO3 is limited to schemas. For content changes you need a 3rd party library/tool.
It only creates tables and doesn't drop them by default.
The first "problem" it says that you can only perform database structure changes but not the content in it. For that, one could use the QueryBuilder.
The second problem is that the getCreateTableStatementArray function in the SqlReader class does the following:
return $this->getStatementArray($dumpContent, '^CREATE TABLE');
This calls the getStatementArray function and it only returns the CREATE TABLE commands. So if you have a DROP TABLE command in the SQL file, it won't be taken into consideration. For that, we need to create our own function for the getCreateTableStatementArray. So in your Task, create this function:
/**
* #param string $dumpContent
* #return array
*/
public function getCreateTableStatementArray(string $dumpContent): array
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
return $sqlReader->getStatementArray($dumpContent);
}
With this, TYPO3 will not evaluate the statements by command name and it will get all the commands available in the sql file.
Now that we have done that, we need to pass the statements into our custom function to be parsed. So somewhere in your code you get file's content. I have it like this:
/**
* #return bool
* #throws \Exception
*/
public function execute()
{
...
$this->createTableStructure();
...
}
protected function createTableStructure()
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$sqlStatements = $this->getCreateTableStatementArray(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlStatements as $sqlStatement) {
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
$connection = $connectionPool->getConnectionByName('Default');
try {
$connection->executeUpdate($sqlStatement);
} catch (DBALException $e) {
//Your log here
}
}
}
For this part i got an error that TYPO3 could not read the DEFAULT attribute so i had to remove it.
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
This looks like this in the SQL file:
DROP TABLE IF EXISTS `myTable`;
CREATE TABLE IF NOT EXISTS `myTable` (
`myColumn1` int(10) NOT NULL DEFAULT '0',
`myColumn2` varchar(255) DEFAULT NULL,
`myColumn3` varchar(255) DEFAULT NULL,
`myColumn4` date DEFAULT NULL,
`myColumn5` varchar(255) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Hope it was helpful!
Best regards

Related

Creating related Models on Laravel/Eloquent relations with primary key type UUID

Laravel: 7; PHP: 7.4/8.0
In my project I have two related models: User and TimeAccount. Both use UUID for their primary key, the key name is still id.
Migrations:
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->uuid('id')->primary();
// snip...
});
}
// snip...
}
class CreateTimeAccountsTable extends Migration
{
public function up()
{
Schema::create('time_accounts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->nullable()->constrained();
// snip...
});
}
// snip...
}
Models:
class User
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function timeAccount()
{
return $this->hasOne(TimeAccount::class);
}
// snip...
}
class TimeAccount
{
use IsIdentifiedByUuid;
protected $keyType = 'string';
public function user()
{
return $this->belongsTo(User::class);
}
// snip...
}
IsIdentifiedByUuid Trait:
trait IsIdentifiedByUuid
{
protected static function bootIsIdentifiedByUuid()
{
static::creating(fn ($model) => static::isIdentifiedByUuidCreatingHandler($model));
static::saving(fn ($model) => static::isIdentifiedByUuidSavingHandler($model));
}
public function initializeIsIdentifiedByUuid()
{
$this->keyType = 'string';
}
protected function getUuidColumn(): string
{
return property_exists($this, 'uuid_column') ? $this->uuid_column : 'id';
}
protected static function getNewUuid(): UuidInterface
{
$columnName = app(static::class)->getUuidColumn();
$tableName = app(static::class)->getTable();
$columnDescriptor = "$tableName.$columnName";
$uuid = "";
$query = \DB::table($tableName)
->select($columnDescriptor)
->where($columnDescriptor, $uuid);
$attempts = 0;
do {
if ($attempts >= \App\Constants\Uuid::MAX_ATTEMPTS) {
throw new UuidMaxGeneratorAttemptsExceededException();
}
$uuid = Str::uuid();
$attempts++;
} while ($query->setBindings([ $uuid->toString() ])->count() > 0);
return $uuid;
}
/**
* Handles the creation of a model.
* - Generates new UUID if the UUID column is empty and auto-increment is enabled
*
* #param Model $model
* #throws \App\Exceptions\UuidMaxGeneratorAttemptsExceededException
*/
protected static function isIdentifiedByUuidCreatingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
if ($model->getIncrementing() && !$model->{$columnName}) {
$uuid = static::getNewUuid();
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Generating new UUID for `" . get_class($model) . ": $uuid`"
);
$model->{$columnName} = $uuid->toString();
} else {
\Log::debug(
"IsIdentifiedByUuid [CREATING]:" .
" Using existing UUID for `" . get_class($model) . ": $model->{$columnName}`"
);
}
}
/**
* Handles the saving of a Model.
* - Prevents changes to the UUID column
* - Rolls back changed value to old value
*
* #param Model $model
*/
protected static function isIdentifiedByUuidSavingHandler(Model $model)
{
$columnName = $model->getUuidColumn();
$originalUuid = $model->getOriginal($columnName);
if (!is_null($originalUuid) &&
$originalUuid !== $model->{$columnName}
) {
\Log::debug(
"IsIdentifiedByUuid [SAVING]:" .
" Prevented change of UUID for `" . get_class($model) . ":$originalUuid`"
);
$model->{$columnName} = $originalUuid;
}
}
}
Now the Problem:
Within an user-observer on the created hook I'm doing this:
$userTimeAccount = $user->timeAccount()->create([
// snip...
]);
So far - before switching to UUID instead of integer keys - everything worked fine! As far as I know creating the related model before the user-model has been saved (and obviously before auto-inc was triggered) is something that is explicitly allowed by eloquent/Laravel (though I can not find the section of the docs).
After switching to UUID this is not working anymore and I'm getting an SQL error:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (laravel_torus.time_accounts, CONSTRAINT time_accounts_user_id_foreign FOREIGN KEY (user_id) REFERENCES users (id)) (SQL: insert into time_accounts (time_account_type_id, balance, borrow, user_id, id, updated_at, created_at) values (3, 0, 0, 0, a995807b-b7e1-4af3-96de-7c48187f943d, 2021-07-09 12:54:25, 2021-07-09 12:54:25))
SQL beautified:
insert into `time_accounts`
(`time_account_type_id`, `balance`, `borrow`, `user_id`, `id`, `updated_at`, `created_at`)
values
(3, 0, 0, 0, 'bad86e70-7496-4b42-b2a1-5cc3c5a4d06c', '2021-07-09 11:19:17', '2021-07-09 11:19:17');
Somehow Laravel tries to insert zero (integer) for the Foreign key which will obviously not work. I even tried setting keyType to 'string' manually without effect.
Where am I wrong?
EDIT: It seems like there is a race condition. The created hook of the Observer will be triggered before the created hook of the User model. This leads to the missing ID - the ID is present when using integer and auto increment, just as it should.
TL;DR
I was not able to solve this with MySQL and switched to Postgres SQL.
DONT USE UUID WITH MYSQL unless you are, absolutely sure that: 1. you need to and 2. the performance impact will be negligible!
I really can not explain how this error is happening at all. The ID in question already exists (looking with the debugger into the $attribnutes array of the model), still Laravel will return integer 0 when calling $user->id.
While investigating the problem I noticed that Laravel is creating String columns for the UUID instead of binary (wich is what I would have expected). Since string comparism is quite cost intensive compared to integer/binary comparism I decided to abandon MySQL. UUID's are mandatory for this project and a shift to another DBMS, away from MySQL, in future was already intended.
Postgres SQL supports a special Column type uuid wich will be used by Laravel if a UUID column is defined. This does not only store and compare the ID as binary but also checks for format and unique values, meaning you can not enter an invalid UUID by accident like in MySQL regular sting columns wich seem to have no checks at all apart from not null and unique.

Why is "deleteBlocks" not working in phpWord?

I´m trying to use blocks in my Word document but I´m having some problems. First of all, when I declare a block in my document, if I don´t use the function "cloneBlock", the result appears like this:
${sec}
example
${/sec}
Maybe I must use that function to appear properly. But my main problem is that "deleteBlock" is not working. If I don´t clone the block, the generated docx is corrupted. But if I clone the block, the function "deleteBlock" doesn´t delete the block and it appear the information that is inside that block in my final docx file.
This is my code:
//Word
// Creating the new document...
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('../example.docx');
//set value
//$templateProcessor->setValue('title', 'Example');
//Triplicate block
$templateProcessor->cloneBlock('firstblock', 3, true, true);
$templateProcessor->setValue('firstname#1', 'John');
$templateProcessor->setValue('lastname#1', 'Doe');
$templateProcessor->setValue('firstname#2', 'John');
$templateProcessor->setValue('lastname#2', 'Doe');
$templateProcessor->setValue('firstname#3', 'John');
$templateProcessor->setValue('lastname#3', 'Doe');
//Delete Block
$templateProcessor->cloneBlock('sec', 1, true, true);
$templateProcessor->deleteBlock('sec');
$templateProcessor->saveAs('example.docx');
Docx template:
${firstblock}
Hello ${firstname} ${lastname}!
${/firstblock}
${sec}
example
${/sec}
UPDATE:
Instead of using the function "deleteBlock", I have use the function "cloneBlock" like this and it deletes the block:
//Delete Block
$templateProcessor->cloneBlock('sec', 0, true, true);
So, I have write to clone the block 0 times, so it disappears
But I have another problem. I don´t know why, but this only works sometimes
I'm not sure why user #d1845412 deleted their previous answer, but it actually solved my issue. I overwrote the deleteBlock method with the following code and it seems to work. I prefer this small change over larger changes to the replaceBlock method.
/**
* Override this method since deleteBlock doesn't seem to work.
* #param string $blockname
*/
public function deleteBlock($blockname)
{
$this->cloneBlock($blockname, 0, true, true);
}
Try this. I slightly changed the regexp in replaceBlock, I also added a function that removes unused patterns, it may come in handy) I'll warn you right away - not really testing so use it carefully
class PatchedTemplateProcessor extends TemplateProcessor
{
/**
* Remove ${*} and ${/*} from temporary document
*/
public function removeSearchPatterns()
{
preg_match_all(
'/(\$\{[^\}]*\})/',
$this->tempDocumentMainPart,
$matches,
PREG_SET_ORDER
);
foreach ($matches as $match){
$this->tempDocumentMainPart = str_replace(
$match,
'',
$this->tempDocumentMainPart
);
}
}
/**
* #param string $blockname
* #param string $replacement
*/
public function replaceBlock($blockname, $replacement)
{
$matches = array();
preg_match(
'/(<w:t.*>\${' . $blockname . '}<\/w:.*?t>)(.*)(<w:t.*\${\/' . $blockname . '}<\/w:.*?t>)/is',
$this->tempDocumentMainPart,
$matches
);
if (isset($matches[3])) {
$this->tempDocumentMainPart = str_replace(
$matches[2] . $matches[3],
$replacement,
$this->tempDocumentMainPart
);
}
}
/**
* Delete a block of text.
*
* #param string $blockname
*/
public function deleteBlock($blockname)
{
$this->replaceBlock($blockname, '');
}
}

Converting SQL to Joomla Syntax (set and update)

Ok so basically I need to convert this regular sql statement to the syntax joomla uses via
https://api.joomla.org/11.4/Joomla-Platform/Database/JDatabaseQuery.html
here is my statement
SET #myunsubid = (
SELECT subid
FROM aqbi8_acymailing_subscriber s
WHERE s.email = 'email#email.co.nz'
);
SELECT #myunsubid;
UPDATE aqbi8_acymailing_listsub a
SET a.`status` = 1
WHERE a.subid = #myunsubid AND a.listid = 232
So id like it to be like
$db->set(#myunsubid = ( $db->select($db->quoteName('subid') )
$db->from($db->quoteName('aqbi8_acymailing_subscriber s') )
$db->where($db->quoteName('s.email') = 'email#email.co.nz')
)
$db->update($db->quoteName('aqbi8_acymailing_listsub a'))
$db->set($db->quoteName('a.status') = 1)
$db->where ($db->quoteName('a.subid') = #myunsubid AND $db->quoteName('a.listid') = 232 )
But this isnt quite right. please help!
I actually figured it out got it to work like this.
$db = &JDatabase::getInstance($option);
$query = $db->getQuery(true);
// make a variable for subID
$query->select($db->quoteName(array('subid')));
$query->from($db->quoteName('aqbi8_acymailing_subscriber'));
$query->where($db->quoteName('email') . " = '" . $email ."'");
$db->setQuery($query);
$db->execute();
$test = $db->loadObjectList();
print_r( $test );
$myid = $test[0]->subid;
$query->clear();
// // Create Database query
$fields = $db->quoteName('status') . ' = 1';
$conditions = array(
$db->quoteName('subid') . ' = ' . $myid,
$db->quoteName('listid') . ' = ' . $listid
);
// // update query
$query->update($db->quoteName('aqbi8_acymailing_listsub'))->set($fields)->where($conditions);
$db->setQuery($query);
$db->execute();
You don't need to make two trips to the database, you can write a subquery into your UPDATE's WHERE condition (no mysql variables or table aliases are necessary).
Raw Query:
UPDATE aqbi8_acymailing_listsub
SET status = 1
WHERE listid = 232
AND subid = (
SELECT subid
FROM aqbi8_acymailing_subscriber
WHERE `email` = 'email#email.co.nz'
)
Tested Code:
$db = JFactory::getDBO();
try {
$subquery = $db->getQuery(true)
->select('subid')
->from('#__acymailing_subscriber')
->where("email = 'email#email.co.nz'");
$query = $db->getQuery(true)
->update("#__acymailing_listsub")
->set("status = 1")
->where(["listid = 232", "personid = ($subquery)"]); // or make 2 where() calls
echo $query->dump(); // if you want to see; *during development ONLY
$db->setQuery($query);
$db->execute();
if ($affrows = $db->getAffectedRows()) {
JFactory::getApplication()->enqueueMessage("Updated. Rows affected: $affrows", 'success');
} else {
JFactory::getApplication()->enqueueMessage("Logic Error", 'error');
}
} catch (Exception $e) {
JFactory::getApplication()->enqueueMessage("Query Syntax Error: " . $e->getMessage(), 'error'); // never show getMessage() to public
}
It is not clear if any of your values are coming from users/untrusted sources, so be sure to follow good practices when writing variables into your queries -- like casting integers with (int) and calling $db->quote() on string values.
If you want to see a complex/convoluted UPDATE query with several other tables and techniques blended in, here is a comprehensive post: https://joomla.stackexchange.com/a/22916/12352
Please DON'T USE JDatabase Object to update Joomla tables, when there's an API available for the extension.
Whilst I appreciate the OP's question is pertaining to how to update the joomla database using the joomla database object (JDatabase), I propose a safer and more robust method, the "ACYMailing API".
"BUT WHY?", I hear you ask...
Good question!!!
There are 2 pitfalls in updating the joomla database directly - be it on the command-line, in a GUI such as MySQL Workbench or PHPMyAdmin, or even with the Joomla Database Object. Simply put, they both concern compatibility - 1. regarding third party integrations, and 2. concerning the future compatibility of your code. In a nutshell, whenever there's a an API for interacting with a component, I'd use it, over JDatabase every time to future proof your code, and ensure that all pre and post save, update, delete... ...move, and publish plugin events take care of your integrations, just as if you'd performed the action authentically.
To elaborate on these points a bit...
Most Joomla extensions (core and 3rd-party) make use of Joomla's powerful plugin architecture. By doing so, extensions can perform actions at key points in the application's life cycle. For example, after deleting a record from a table belonging to component1, delete related records from a table relating to compnent2. Therefore, one run's the risk of breaking the behaviour/functionality of the component in question - i.e. ACY Mailing, as in your case. Potentially, other core/3rd-party extensions that rely on ACY's data, that would otherwise, get updated through onAfterSave() or onAfterDelete() plugin events, as they will not get called.
There's a big risk that your code to break with future Joomla/ACY Mailing updates, if/when the table structure changes.
OK, so how do we use the API?
The following example code displays everything that you should need to update a subscription record. Each step explains the code, which for reference, is summarised in doc and inline comments in the code itself. To begin, navigate to the file where you are entering your code, then...
STEP BY STEP
STEP 1: Check the existence of ACY Mailing by attempting to include it's helper class, as follows. N.B. If the include_once() fails, you should see the echo statement, indicating that ACY Mailing IS NOT installed.
// load the ACY Mailing helper - bail out if not
if(!include_once(rtrim(JPATH_ADMINISTRATOR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'components' . DIRECTORY_SEPARATOR . 'com_acymailing' . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'helper.php')){
echo 'This code can not work without the AcyMailing Component';
return false;
}
STEP 2: Set-up your parameters by inputting values into the following 3 variables. See examples in code comments.
// array $lists An array of integer IDs (primary keys) of the lists you want the user to be subscribed to (can be empty).
// e.g. array(2,4,6)
$lists = array();
// array $unsubs An array of integer IDs (primary keys) of the lists you want the user to be un-subscribed from (can be empty).
// e.g. array(2,4,6)
$unsubs = array();
// string $userID Numeric Joomla User or user e-mail. For example: '42' or 'name#domain.com'
$userID = '';
STEP 3: Add the following code to find the ACY Mailing user, from the Joomla User ID/Email address passed in to the ->subid() method, and bail out if not found.
// instantiate the ACY Mailing Subscriber (user) Class
$user = acymailing_get('class.subscriber');
// find the ACY Mailing user id (subid) from the joomla ID or email address set in $userID
$subID = $user->subid($userID);
// No ACY Mailing user/subscriber?
if(empty($subID))
return; // bail out
STEP 4: Add the following code to check, and setup the data for any of the subscriptions/unsubscriptions you've configured to update ($lists and $unsubs arrays). If any found, they will be updated. If not found, return.
// create an array to store data in
$data = array();
// Set up new newsletter subscriptions from the $lists array()
if(!empty($lists)) foreach($lists as $listId)
$data[$listId] = array("status" => 1);
// Set up un-subscriptions from the $unsubs array()
if(!empty($unsubs)) foreach($unsubs as $listId)
$data[$listId] = array('status' => 0);
// no data, bail out...
if(empty($data))
return; //there is nothing to do...
// update the user's subscription records, creating/removing subscriptions/unsubsriptions accordingly
$user->saveSubscription($subID, $data);

Propel ORM - how do I specify tables to reverse engineer?

I have a database with a few dozen tables. My app deals with just one of those tables. I'd like propel to generate the schema just for that table. Is there a way I can specify the table(s) in build.properties?
A simple extension of Propel (code examples based on Propel-1.6.8) to allow ignore a custom set of tables (configured using a property):
Extend build-propel.xml:
<!-- Ignore Tables Feature -->
<taskdef
name="propel-schema-reverse-ignore"
classname="task.PropelSchemaReverseIgnoreTask" classpathRef="propelclasses"/>
Extend build.xml:
<target name="reverse-ignore" depends="configure">
<phing phingfile="build-propel.xml" target="reverse-ignore"/>
</target>
Extend PropelSchemaReverseTask:
class PropelSchemaReverseIgnoreTask extends PropelSchemaReverseTask {
/**
* Builds the model classes from the database schema.
* #return Database The built-out Database (with all tables, etc.)
*/
protected function buildModel()
{
// ...
// Loads Classname reverse.customParserClass if present
$customParserClassname = $config->getBuildProperty("reverseCustomParserClass");
if ($customParserClassname!=null) {
$this->log('Using custom parser class: '.$customParserClassname);
$parser = $config->getConfiguredSchemaParserForClassname($con, $customParserClassname);
} else {
$parser = $config->getConfiguredSchemaParser($con);
}
// ...
}
}
Add function to GeneratorConfig:
class GeneratorConfig implements GeneratorConfigInterface {
// ...
/**
* Ignore Tables Feature:
* Load a specific SchemaParser class
*
* #param PDO $con
* #param string $clazzName SchemaParser class to load
* #throws BuildException
* #return Ambigous <SchemaParser, unknown>
*/
public function getConfiguredSchemaParserForClassname(PDO $con = null, $clazzName)
{
$parser = new $clazzName();
if (!$parser instanceof SchemaParser) {
throw new BuildException("Specified platform class ($clazz) does implement SchemaParser interface.", $this->getLocation());
}
$parser->setConnection($con);
$parser->setMigrationTable($this->getBuildProperty('migrationTable'));
$parser->setGeneratorConfig($this);
return $parser;
}
}
Extend MysqlSchemaParser:
class MysqlSchemaIgnoreParser extends MysqlSchemaParser {
public function parse(Database $database, Task $task = null)
{
$this->addVendorInfo = $this->getGeneratorConfig()->getBuildProperty('addVendorInfo');
$stmt = $this->dbh->query("SHOW FULL TABLES");
// First load the tables (important that this happen before filling out details of tables)
$tables = array();
$configIgnoreTables = $this->getGeneratorConfig()->getBuildProperty("reverseIgnoreTables");
$tables_ignore_list = array();
$tables_ignore_list = explode(",", $configIgnoreTables);
if ($task) {
$task->log("Reverse Engineering Tables", Project::MSG_VERBOSE);
}
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$name = $row[0];
$type = $row[1];
if (!in_array($name, $tables_ignore_list)) {
// ...
} else {
$task->log("Ignoring table: " .$name);
}
}
// ...
}
}
Add new (optional) properties to build.properties:
# Propel Reverse Custom Properties
propel.reverse.customParserClass=MysqlSchemaIgnoreParser
propel.reverse.ignoreTables = table1,table2
A solution could be to write your own schema parser that ignores certain tables. These tables to exclude could be read from a config file or so.
Take a look at the MysqlSchemaParser.php to get an idea of how such a parser works. You could extend such an existing parser and just override the parse() method. When tables are added, you would check them for exclusion/inclusion and only add them if they meet your subset criteria.
How to create custom propel tasks is described in the Propel cookbook.
Instead of invoking propel-gen reverse you would then invoke your customized reverse engineering task.
If done neatly, I'd find it worth being added as a contribution to the Propel project and you'd definitely deserve fame for that! :-)

Using boolean fields with Magento ORM

I am working on a backend edit page for my custom entity. I have almost everything working, including saving a bunch of different text fields. I have a problem, though, when trying to set the value of a boolean field.
I have tried:
$landingPage->setEnabled(1);
$landingPage->setEnabled(TRUE);
$landingPage->setEnabled(0);
$landingPage->setEnabled(FALSE);
None seem to persist a change to my database.
How are you supposed to set a boolean field using magento ORM?
edit
Looking at my database, mysql is storing the field as a tinyint(1), so magento may be seeing this as an int not a bool. Still can't get it to set though.
This topic has bring curiosity to me. Although it has been answered, I'd like to share what I've found though I didn't do intense tracing.
It doesn't matter whether the cache is enabled / disabled, the table schema will be cached.
It will be cached during save process.
Mage_Core_Model_Abstract -> save()
Mage_Core_Model_Resource_Db_Abstract -> save(Mage_Core_Model_Abstract $object)
Mage_Core_Model_Resource_Db_Abstract
public function save(Mage_Core_Model_Abstract $object)
{
...
//any conditional will eventually call for:
$this->_prepareDataForSave($object);
...
}
protected function _prepareDataForSave(Mage_Core_Model_Abstract $object)
{
return $this->_prepareDataForTable($object, $this->getMainTable());
}
Mage_Core_Model_Resource_Abstract
protected function _prepareDataForTable(Varien_Object $object, $table)
{
$data = array();
$fields = $this->_getWriteAdapter()->describeTable($table);
foreach (array_keys($fields) as $field) {
if ($object->hasData($field)) {
$fieldValue = $object->getData($field);
if ($fieldValue instanceof Zend_Db_Expr) {
$data[$field] = $fieldValue;
} else {
if (null !== $fieldValue) {
$fieldValue = $this->_prepareTableValueForSave($fieldValue, $fields[$field]['DATA_TYPE']);
$data[$field] = $this->_getWriteAdapter()->prepareColumnValue($fields[$field], $fieldValue);
} else if (!empty($fields[$field]['NULLABLE'])) {
$data[$field] = null;
}
}
}
}
return $data;
}
See the line: $fields = $this->_getWriteAdapter()->describeTable($table);
Varien_Db_Adapter_Pdo_Mysql
public function describeTable($tableName, $schemaName = null)
{
$cacheKey = $this->_getTableName($tableName, $schemaName);
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
if ($ddl === false) {
$ddl = parent::describeTable($tableName, $schemaName);
/**
* Remove bug in some MySQL versions, when int-column without default value is described as:
* having default empty string value
*/
$affected = array('tinyint', 'smallint', 'mediumint', 'int', 'bigint');
foreach ($ddl as $key => $columnData) {
if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== FALSE)) {
$ddl[$key]['DEFAULT'] = null;
}
}
$this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
}
return $ddl;
}
As we can see:
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
will try to load the schema from cache.
If the value is not exists: if ($ddl === false)
it will create one: $this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
So the problem that occurred in this question will be happened if we ever save the model that is going to be altered (add column, etc).
Because it has ever been $model->save(), the schema will be cached.
Later after he add new column and "do saving", it will load the schema from cache (which is not containing the new column) and resulting as: the data for new column is failed to be saved in database
Delete var/cache/* - your DB schema is cached by Magento even though the new column is already added to the MySQL table.