Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
richardhj committed Nov 7, 2019
2 parents 944371f + b25e8ca commit 8fef15a
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 72 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Changelog
=========

See also the [GitHub releases page](https://github.com/terminal42/contao-DC_Multilingual/releases).

4.0.0
-----

* Added the changelog ;-) (#56)
* Raised minimum PHP version to 5.6.
* Raised minimum Contao version to 4.4.
* Multilingual aliases have been improved, so language records can have the same alias as main record. (#47)
* The models are not prevented from saving by default anymore. They are only prevented if fetched from database. (#51)
* Allow to count records using subqueries, e.g. with `HAVING` clause.
* **BC break:** Removed the deprecated `pidColumn` configuration. Use `langPid` instead.
* **BC break:** Added the new interface method `MultilingualQueryBuilderInterface::buildQueryBuilderForCountWithSubQuery()`.
* **BC break:** Renamed the translation table alias from `t2` to `translation`.
45 changes: 25 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,35 @@ $GLOBALS['TL_DCA']['table']['fields']['name']['eval']['translatableFor'] = ['de'
## Example usage

```php
// Update tl_user configuration
$GLOBALS['TL_DCA']['tl_user']['config']['dataContainer'] = 'Multilingual';
$GLOBALS['TL_DCA']['tl_user']['config']['languages'] = array('en', 'de', 'pl');
$GLOBALS['TL_DCA']['tl_user']['config']['langPid'] = 'langPid';
$GLOBALS['TL_DCA']['tl_user']['config']['langColumnName'] = 'language';
$GLOBALS['TL_DCA']['tl_user']['config']['fallbackLang'] = 'en';
// Update tl_news configuration
$GLOBALS['TL_DCA']['tl_news']['config']['dataContainer'] = 'Multilingual';
$GLOBALS['TL_DCA']['tl_news']['config']['languages'] = ['en', 'de', 'pl'];
$GLOBALS['TL_DCA']['tl_news']['config']['langPid'] = 'langPid';
$GLOBALS['TL_DCA']['tl_news']['config']['langColumnName'] = 'language';
$GLOBALS['TL_DCA']['tl_news']['config']['fallbackLang'] = 'en';

// Add the language fields
$GLOBALS['TL_DCA']['tl_user']['config']['sql']['keys']['langPid'] = 'index';
$GLOBALS['TL_DCA']['tl_user']['config']['sql']['keys']['language'] = 'index';
$GLOBALS['TL_DCA']['tl_user']['fields']['langPid']['sql'] = "int(10) unsigned NOT NULL default '0'";
$GLOBALS['TL_DCA']['tl_user']['fields']['language']['sql'] = "varchar(2) NOT NULL default ''";
$GLOBALS['TL_DCA']['tl_news']['config']['sql']['keys']['langPid'] = 'index';
$GLOBALS['TL_DCA']['tl_news']['config']['sql']['keys']['language'] = 'index';
$GLOBALS['TL_DCA']['tl_news']['fields']['langPid']['sql'] = "int(10) unsigned NOT NULL default '0'";
$GLOBALS['TL_DCA']['tl_news']['fields']['language']['sql'] = "varchar(2) NOT NULL default ''";

// Make some fields translatable
$GLOBALS['TL_DCA']['tl_user']['fields']['username']['eval']['translatableFor'] = '*';
$GLOBALS['TL_DCA']['tl_user']['fields']['name']['eval']['translatableFor'] = array('de');
$GLOBALS['TL_DCA']['tl_news']['fields']['headline']['eval']['translatableFor'] = '*';
$GLOBALS['TL_DCA']['tl_news']['fields']['subheadline']['eval']['translatableFor'] = ['de'];
```

## Querying using the model

```php
class UserModel extends Terminal42\DcMultilingualBundle\Model\Multilingual
class NewsModel extends Terminal42\DcMultilingualBundle\Model\Multilingual
{
protected static $strTable = 'tl_user';
protected static $strTable = 'tl_news';

public static function findActive()
{
$arrColumns = array("t1.disable=''");
return static::findBy($arrColumns, null);
}
public static function findPublished()
{
return static::findBy(['t1.published=?'], [1]);
}
}
```

Expand All @@ -76,7 +75,7 @@ translations are filtered so you only see the fallback language there.
When querying using the `Multilingual` model or using the
`MultilingualQueryBuilder`, the same table is simply joined so we have the
fallback language aliased as `t1` and the target language (which you specify
explicitly or it uses the current page's language) aliased as `t2`. Now, using
explicitly or it uses the current page's language) aliased as `translation`. Now, using
MySQL's `IFNULL()` function, it checks whether there's a translated value and
if not, automatically falls back to the fallback language. This allows you to
translate only a subset of fields.
Expand Down Expand Up @@ -129,3 +128,9 @@ In the front end you can then search by a multilingual alias like this:
MyModel::findByMultilingualAlias($alias);
```

## Useful notes

1. Sometimes a table you want to make multilingual already contains the `language` field (e.g. `tl_user`),
which may lead to unexpected results. In such cases you have to make sure that data container's property
`$GLOBALS['TL_DCA']['tl_table']['config']['langColumnName']` is set to something else than `language`.
See [#53](https://github.com/terminal42/contao-DC_Multilingual/issues/53) for more details.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
}
],
"require":{
"php":">=5.5",
"contao/core-bundle":"^4.1",
"php": "^5.6 || ^7.0",
"contao/core-bundle": "^4.4",
"doctrine/dbal": "^2.5"
},
"require-dev": {
Expand Down
76 changes: 57 additions & 19 deletions src/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -486,21 +486,30 @@ public function edit($intId=null, $ajaxId=null)
if (isset($_POST['saveNclose']))
{
\Message::reset();
\System::setCookie('BE_PAGE_OFFSET', 0, 0);

if (version_compare(VERSION, '4.8', '<')) {
\System::setCookie('BE_PAGE_OFFSET', 0, 0);
}

$this->redirect($this->getReferer());
}
elseif (isset($_POST['saveNedit']))
{
\Message::reset();
\System::setCookie('BE_PAGE_OFFSET', 0, 0);

if (version_compare(VERSION, '4.8', '<')) {
\System::setCookie('BE_PAGE_OFFSET', 0, 0);
}

$this->redirect($this->addToUrl($GLOBALS['TL_DCA'][$this->strTable]['list']['operations']['edit']['href'], false, array('s2e', 'act')));
}
elseif (isset($_POST['saveNback']))
{
\Message::reset();
\System::setCookie('BE_PAGE_OFFSET', 0, 0);

if (version_compare(VERSION, '4.8', '<')) {
\System::setCookie('BE_PAGE_OFFSET', 0, 0);
}

if ($this->ptable == '')
{
Expand All @@ -519,7 +528,10 @@ public function edit($intId=null, $ajaxId=null)
elseif (isset($_POST['saveNcreate']))
{
\Message::reset();
\System::setCookie('BE_PAGE_OFFSET', 0, 0);

if (version_compare(VERSION, '4.8', '<')) {
\System::setCookie('BE_PAGE_OFFSET', 0, 0);
}

$strUrl = TL_SCRIPT . '?do=' . \Input::get('do');

Expand Down Expand Up @@ -657,12 +669,6 @@ protected function copyChilds($table, $insertID, $id, $parentId)

if ($GLOBALS['TL_DCA'][$table]['config']['langPid']) {
$pidColumnName = $GLOBALS['TL_DCA'][$table]['config']['langPid'];
} elseif ($GLOBALS['TL_DCA'][$table]['config']['pidColumn']) {
$pidColumnName = $GLOBALS['TL_DCA'][$table]['config']['pidColumn'];

// The pidColumn is deprecated
// @todo - deprecated, remove in 3.0
trigger_error('The "pidColumn" setting is deprecated, use "langPid" instead.', E_USER_NOTICE);
} else {
$pidColumnName = $this->pidColumnName;
}
Expand Down Expand Up @@ -990,18 +996,56 @@ protected function save($varValue)
$varValue = \StringUtil::generateAlias($this->objActiveRecord->{$fromField});
}

$records = \Database::getInstance()
->prepare("SELECT id FROM {$this->strTable} WHERE {$this->pidColumnName}=?")
->execute(($this->objActiveRecord->{$this->pidColumnName} > 0) ? $this->objActiveRecord->{$this->pidColumnName} : $this->objActiveRecord->id)
;

$excludedIds = $records->fetchEach('id');

if ($this->objActiveRecord->{$this->pidColumnName} > 0) {
$excludedIds[] = $this->objActiveRecord->{$this->pidColumnName};
} else {
$excludedIds[] = $this->objActiveRecord->id;
}

// Check for duplicates in current language
$objAlias = \Database::getInstance()->prepare(
"SELECT id FROM {$this->strTable} WHERE id!=? AND {$this->strField}=? AND {$this->langColumnName}=?"
)->execute($this->objActiveRecord->id, $varValue, $this->currentLang);
"SELECT id FROM {$this->strTable} WHERE id NOT IN (" . implode(',', $excludedIds) . ") AND {$this->strField}=? AND {$this->langColumnName}=?"
)->execute($varValue, $this->currentLang);

$skipAliasValidation = false;

// Check whether the alias exists
if ($objAlias->numRows > 0) {
if (!$autoAlias) {
throw new \InvalidArgumentException(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue));
}

$varValue .= '-' . $this->intId;
// For child record take the parent record alias
if ($this->objActiveRecord->{$this->pidColumnName} > 0) {
$parent = \Database::getInstance()
->prepare("SELECT {$this->strField} FROM {$this->strTable} WHERE id=?")
->execute($this->objActiveRecord->{$this->pidColumnName})
;

if ($parent->numRows) {
$varValue = $parent->{$this->strField};
$skipAliasValidation = true;
}
} else {
$varValue .= '-' . $this->intId;
}
} else {
$skipAliasValidation = true;
}

// Skip the further alias validation
if ($skipAliasValidation) {
$GLOBALS['TL_DCA'][$this->strTable]['fields'][$this->strField]['eval']['unique'] = false;

// Avoid alias validation in callbacks as well
unset($GLOBALS['TL_DCA'][$this->strTable]['fields'][$this->strField]['save_callback']);
}

parent::save($varValue);
Expand Down Expand Up @@ -1042,12 +1086,6 @@ public function deleteChilds($table, $id, &$delete)
// be a child table
if ($GLOBALS['TL_DCA'][$table]['config']['langPid']) {
$pidColumnName = $GLOBALS['TL_DCA'][$table]['config']['langPid'];
} elseif ($GLOBALS['TL_DCA'][$table]['config']['pidColumn']) {
$pidColumnName = $GLOBALS['TL_DCA'][$table]['config']['pidColumn'];

// The pidColumn is deprecated
// @todo - deprecated, remove in 3.0
trigger_error('The "pidColumn" setting is deprecated, use "langPid" instead.', E_USER_NOTICE);
} else {
$pidColumnName = $this->pidColumnName;
}
Expand Down
72 changes: 52 additions & 20 deletions src/Model/Multilingual.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,14 @@
namespace Terminal42\DcMultilingualBundle\Model;

use Contao\Database;
use Contao\Database\Result;
use Contao\Model;
use Contao\Model\Collection;
use Doctrine\DBAL\Query\QueryBuilder;
use Terminal42\DcMultilingualBundle\QueryBuilder\MultilingualQueryBuilderFactoryInterface;

class Multilingual extends \Model
class Multilingual extends Model
{
/**
* Prevent the model from saving.
*
* @param \Database\Result $objResult An optional database result
*/
public function __construct(\Database\Result $objResult = null)
{
parent::__construct($objResult);

$this->preventSaving(false);
}

/**
* Returns the ID of the fallback language.
*/
Expand Down Expand Up @@ -90,9 +81,10 @@ public function getAlias($language, $aliasColumnName = 'alias')
*/
public static function findByAlias($alias, $aliasColumnName = 'alias', $options = [])
{
$table = static::getTable();
$options = array_merge([
'limit' => 1,
'column' => ["t1.$aliasColumnName=?"],
'column' => ["($table.$aliasColumnName=?"],
'value' => [$alias],
'return' => 'Model',
],
Expand All @@ -113,9 +105,10 @@ public static function findByAlias($alias, $aliasColumnName = 'alias', $options
*/
public static function findByMultilingualAlias($alias, $aliasColumnName = 'alias', $options = [])
{
$table = static::getTable();
$options = array_merge([
'limit' => 1,
'column' => ["(t1.$aliasColumnName=? OR t2.$aliasColumnName=?)"],
'column' => ["($table.$aliasColumnName=? OR translation.$aliasColumnName=?)"],
'value' => [$alias, $alias],
'return' => 'Model',
],
Expand Down Expand Up @@ -205,13 +198,51 @@ protected static function buildCountQuery(array $options)
{
$mlqb = static::getMultilingualQueryBuilder();

$mlqb->buildQueryBuilderForCount();

static::applyOptionsToQueryBuilder($mlqb->getQueryBuilder(), $options);
if (isset($options['having'])) {
$mlqb->buildQueryBuilderForCountWithSubQuery(static::buildFindQuery($options));
} else {
$mlqb->buildQueryBuilderForCount();
static::applyOptionsToQueryBuilder($mlqb->getQueryBuilder(), $options);
}

return $mlqb->getQueryBuilder();
}

/**
* Prevent model from saving when creating a model from a database result. See #51
*
* @param Result $objResult The database result object
*
* @return static The model
*/
protected static function createModelFromDbResult(Result $objResult)
{
$model = new static($objResult);
$model->preventSaving(false);

return $model;
}

/**
* Prevent new models from saving when creating a new collection from a database result. See #51
*
* @param Result $objResult The database result object
* @param string $strTable The table name
*
* @return Collection The model collection
*/
protected static function createCollectionFromDbResult(Result $objResult, $strTable)
{
$collection = Collection::createFromDbResult($objResult, $strTable);

/** @var self $model */
foreach ($collection as $model) {
$model->preventSaving(false);
}

return $collection->reset();
}

/**
* Apply the model options to the query builder.
*
Expand All @@ -227,8 +258,9 @@ protected static function applyOptionsToQueryBuilder(QueryBuilder $qb, array $op
$qb->andWhere($column);
}
} else {
// Default is likely t1
$qb->andWhere('t1.'.$options['column'].'=?');
// Default is likely fallback table
$table = static::getTable();
$qb->andWhere("$table.{$options['column']}=?");
}
}

Expand Down
Loading

0 comments on commit 8fef15a

Please sign in to comment.