Skip to content

Commit

Permalink
added parameter-list tag to schema parser and aggregate multiple colu…
Browse files Browse the repository at this point in the history
…mns behavior
  • Loading branch information
mringler committed Mar 3, 2021
1 parent 394a6ce commit f33f2d2
Show file tree
Hide file tree
Showing 10 changed files with 1,013 additions and 49 deletions.
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ parameters:
excludes_analyse:
- '%rootDir%/../../../src/Propel/Generator/Command/templates/*'
- '%rootDir%/../../../src/Propel/Generator/Behavior/AggregateColumn/templates/*'
- '%rootDir%/../../../src/Propel/Generator/Behavior/AggregateMultipleColumns/templates/*'
- '%rootDir%/../../../src/Propel/Generator/Behavior/I18n/templates/*'
- '%rootDir%/../../../src/Propel/Generator/Builder/Om/templates/*'
- '%rootDir%/../../../src/Propel/Generator/Behavior/Validate/templates/*'
Expand Down
16 changes: 16 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@
<code>$variableName</code>
</UndefinedGlobalVariable>
</file>
<file src="src/Propel/Generator/Behavior/AggregateMultipleColumns/templates/objectCompute.php">
<UndefinedGlobalVariable occurrences="4">
<code>$aggregationName</code>
<code>$aggregationName</code>
<code>$bindings</code>
<code>$sql</code>
</UndefinedGlobalVariable>
</file>
<file src="src/Propel/Generator/Behavior/AggregateMultipleColumns/templates/objectUpdate.php">
<UndefinedGlobalVariable occurrences="4">
<code>$aggregationName</code>
<code>$aggregationName</code>
<code>$aggregationName</code>
<code>$columnPhpNames</code>
</UndefinedGlobalVariable>
</file>
<file src="src/Propel/Generator/Behavior/Archivable/ArchivableBehavior.php">
<UndefinedPropertyAssignment occurrences="1">
<code>$archiveTable-&gt;isArchiveTable</code>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
<?php

/**
* MIT License. This file is part of the Propel package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Propel\Generator\Behavior\AggregateMultipleColumns;

use InvalidArgumentException;
use Propel\Generator\Behavior\AggregateColumn\AggregateColumnRelationBehavior;
use Propel\Generator\Builder\Om\ObjectBuilder;
use Propel\Generator\Model\Behavior;

/**
* Keeps an aggregate column updated with related table
*
* @author François Zaninotto
*/
class AggregateMultipleColumnsBehavior extends Behavior
{
public const PARAMETER_KEY_FOREIGN_TABLE = 'foreign_table';
public const PARAMETER_KEY_FOREIGN_SCHEMA = 'foreign_schema';
public const PARAMETER_KEY_CONDITION = 'condition';
public const PARAMETER_KEY_COLUMNS = 'columns';
public const PARAMETER_KEY_COLUMN_NAME = 'column_name';
public const PARAMETER_KEY_COLUMN_EXPRESSION = 'expression';

/**
* Keeps track of inserted aggregation functions to avoid naming collisions.
*
* @var array
*/
private static $insertedAggregationNames = [];

/**
* Default parameters value.
*
* @var array
*/
protected $parameters = [
self::PARAMETER_KEY_FOREIGN_TABLE => null,
self::PARAMETER_KEY_FOREIGN_SCHEMA => null,
self::PARAMETER_KEY_CONDITION => null,
self::PARAMETER_KEY_COLUMNS => null,
];

/**
* @var string|null
*/
private $aggregationName;

/**
* Multiple aggregates on the same table is OK.
*
* @return bool
*/
public function allowMultiple()
{
return true;
}

/**
* @return string
*/
public function getAggregationName()
{
if ($this->aggregationName === null) {
$this->aggregationName = $this->buildAggregationName();
}

return $this->aggregationName;
}

/**
* @return string
*/
private function buildAggregationName()
{
$foreignTableName = $this->getForeignTable()->getPhpName();
$baseAggregationName = 'AggregatedColumnsFrom' . $foreignTableName;
$tableName = $this->getTable()->getPhpName();
if (!array_key_exists($tableName, self::$insertedAggregationNames)) {
self::$insertedAggregationNames[$tableName] = [];
}

$existingNames = &self::$insertedAggregationNames[$tableName];
if (!in_array($baseAggregationName, $existingNames)) {
$existingNames[] = $baseAggregationName;

return $baseAggregationName;
}

$ix = 1;
do {
$aggregationName = $baseAggregationName . $ix;
$ix++;
} while (in_array($aggregationName, $existingNames));

$existingNames[] = $aggregationName;

return $aggregationName;
}

/**
* Add the aggregate key to the current table
*
* @return void
*/
public function modifyTable()
{
$this->validateColumnParameter();
$this->addMissingColumnsToTable();
$this->addAutoupdateBehaviorToForeignTable();
}

/**
* @return void
*/
private function validateColumnParameter()
{
$columnParameters = $this->getParameter(self::PARAMETER_KEY_COLUMNS);
if (empty($columnParameters)) {
$this->throwInvalidArgumentExceptionWithLocation('At least one column is required');
}
foreach ($columnParameters as $columnDefinition) {
if (empty($columnDefinition[self::PARAMETER_KEY_COLUMN_NAME])) {
$this->throwInvalidArgumentExceptionWithLocation('Parameter \'name\' is missing on a column');
}
if (empty($columnDefinition[self::PARAMETER_KEY_COLUMN_EXPRESSION])) {
$colName = $columnDefinition[self::PARAMETER_KEY_COLUMN_NAME];
$this->throwInvalidArgumentExceptionWithLocation('Parameter \'expression\' is missing on column ' . $colName);
}
}
}

/**
* Add the aggregate column if not present
*
* @return void
*/
private function addMissingColumnsToTable()
{
$table = $this->getTable();
$columnParameters = $this->getParameter(self::PARAMETER_KEY_COLUMNS);
foreach ($columnParameters as $columnDefinition) {
$columnName = $columnDefinition[self::PARAMETER_KEY_COLUMN_NAME];
if ($table->hasColumn($columnName)) {
continue;
}

$table->addColumn(['name' => $columnName, 'type' => 'INTEGER']);
}
}

/**
* Add a behavior in the foreign table to autoupdate the aggregate column
*
* @return void
*/
private function addAutoupdateBehaviorToForeignTable()
{
if (!$this->getParameter(self::PARAMETER_KEY_FOREIGN_TABLE)) {
$this->throwInvalidArgumentExceptionWithLocation('You must define a \'foreign_table\' parameter');
}
$foreignTable = $this->getForeignTable();
if ($foreignTable->hasBehavior('concrete_inheritance_parent')) {
return;
}

$relationBehavior = new AggregateColumnRelationBehavior();
$relationBehavior->setName('aggregate_multiple_columns_relation');
$relationBehavior->setId('aggregate_multiple_columns_relation_' . $this->getId());
$relationBehavior->addParameter(['name' => 'foreign_table', 'value' => $this->getTable()->getName()]);
$relationBehavior->addParameter(['name' => 'aggregate_name', 'value' => $this->getAggregationName()]);
$relationBehavior->addParameter(['name' => 'update_method', 'value' => 'update' . $this->getAggregationName()]);
$foreignTable->addBehavior($relationBehavior);
}

/**
* @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
*
* @return string
*/
public function objectMethods(ObjectBuilder $builder)
{
$script = '';
$script .= $this->addObjectCompute($builder);
$script .= $this->addObjectUpdate();

return $script;
}

/**
* @param \Propel\Generator\Builder\Om\ObjectBuilder $builder
*
* @throws \InvalidArgumentException
*
* @return string
*/
protected function addObjectCompute(ObjectBuilder $builder)
{
if ($this->getForeignKey()->isPolymorphic()) {
throw new InvalidArgumentException('AggregateColumnBehavior does not work with polymorphic relations.');
}

$conditions = [];
if ($this->getParameter(self::PARAMETER_KEY_CONDITION)) {
$conditions[] = $this->getParameter(self::PARAMETER_KEY_CONDITION);
}

$bindings = [];
foreach ($this->getForeignKey()->getMapping() as $index => $mapping) {
[$localColumn, $foreignColumn] = $mapping;
$conditions[] = $localColumn->getFullyQualifiedName() . ' = :p' . ($index + 1);
$bindings[$index + 1] = $foreignColumn->getPhpName();
}

$foreignTableName = $this->getForeignTableNameFullyQualified();

$sql = sprintf(
'SELECT %s FROM %s WHERE %s',
$this->buildSelectionStatement(),
$builder->getTable()->quoteIdentifier($foreignTableName),
implode(' AND ', $conditions)
);

return $this->renderTemplate('objectCompute', [
'aggregationName' => $this->getAggregationName(),
'sql' => $sql,
'bindings' => $bindings,
]);
}

/**
* @return string
*/
private function buildSelectionStatement()
{
$columnDefinitions = $this->getParameter(self::PARAMETER_KEY_COLUMNS);
$selects = [];
$table = $this->getTable();
foreach ($columnDefinitions as $columnDefinition) {
$expression = $columnDefinition[self::PARAMETER_KEY_COLUMN_EXPRESSION];
$columName = $columnDefinition[self::PARAMETER_KEY_COLUMN_NAME];
$columnPhpName = $table->getColumn($columName)->getPhpName();
$selects[] = "$expression AS $columnPhpName";
}

return implode(', ', $selects);
}

/**
* @return string
*/
protected function addObjectUpdate()
{
$table = $this->getTable();
$columnPhpNames = array_map(function (array $colDef) use ($table) {
$columName = $colDef[AggregateMultipleColumnsBehavior::PARAMETER_KEY_COLUMN_NAME];

return $table->getColumn($columName)->getPhpName();
},
$this->getParameter(self::PARAMETER_KEY_COLUMNS));

return $this->renderTemplate('objectUpdate', [
'aggregationName' => $this->getAggregationName(),
'columnPhpNames' => $columnPhpNames,
]);
}

/**
* @return string
*/
private function getForeignTableNameFullyQualified()
{
$database = $this->getTable()->getDatabase();
$foreignTableName = $database->getTablePrefix() . $this->getParameter(self::PARAMETER_KEY_FOREIGN_TABLE);
$platform = $database->getPlatform();
$foreignSchema = $this->getParameter(self::PARAMETER_KEY_FOREIGN_SCHEMA);
if ($platform->supportsSchemas() && $foreignSchema) {
$foreignTableName = $foreignSchema . $platform->getSchemaDelimiter() . $foreignTableName;
}

return $foreignTableName;
}

/**
* @return \Propel\Generator\Model\Table|null
*/
protected function getForeignTable()
{
$database = $this->getTable()->getDatabase();
$foreignTableName = $this->getForeignTableNameFullyQualified();

return $database->getTable($foreignTableName);
}

/**
* @return \Propel\Generator\Model\ForeignKey
*/
protected function getForeignKey()
{
$foreignTable = $this->getForeignTable();
// let's infer the relation from the foreign table
$fks = $foreignTable->getForeignKeysReferencingTable($this->getTable()->getName());
if (!$fks) {
$msg = 'You must define a foreign key from the \'%s\' table to the table witht the aggregated columns';
$this->throwInvalidArgumentExceptionWithLocation($msg, $foreignTable->getName());
}

// FIXME doesn't work when more than one fk to the same table
return array_shift($fks);
}

/**
* @param string $format
* @param mixed ...$args
*
* @throws \InvalidArgumentException
*
* @return void
*/
private function throwInvalidArgumentExceptionWithLocation($format, ...$args)
{
$format .= ' in the \'aggregate_multiple_columns\' behavior definition in the \'%s\' table definition';
$args[] = $this->getTable()->getName();
$message = vsprintf($format, $args);

throw new InvalidArgumentException($message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

/**
* Computes the value of the aggregate columns defined as <?=$aggregationName?> ?>
*
* @param ConnectionInterface $con A connection object
*
* @return array The result row from the aggregate query
*/
public function compute<?=$aggregationName?>(ConnectionInterface $con)
{
$stmt = $con->prepare('<?=$sql?>');
<?php foreach ($bindings as $key => $binding):?>
$stmt->bindValue(':p<?=$key?>', $this->get<?=$binding?>());
<?php endforeach;?>
$stmt->execute();

return $stmt->fetch(\PDO::FETCH_NUM);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

/**
* Updates the aggregate columns defined as <?= $aggregationName ?>
*
* @param ConnectionInterface $con A connection object
*/
public function update<?= $aggregationName ?>(ConnectionInterface $con)
{
$aggregatedValues = $this->compute<?= $aggregationName ?>($con);
<?php foreach ($columnPhpNames as $index => $columnName): ?>
$this->set<?= $columnName ?>($aggregatedValues[<?= $index ?>]);
<?php endforeach; ?>
$this->save($con);
}
Loading

0 comments on commit f33f2d2

Please sign in to comment.