Skip to content

Commit

Permalink
Merge pull request #159 from ndm2/entity-property-hints
Browse files Browse the repository at this point in the history
Entity property hints
  • Loading branch information
lorenzo committed Sep 12, 2015
2 parents 2567b5f + 616714b commit 3b03927
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 1 deletion.
5 changes: 4 additions & 1 deletion src/Shell/Task/BakeTemplateTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ public function getView()
$theme = isset($this->params['theme']) ? $this->params['theme'] : '';

$viewOptions = [
'helpers' => ['Bake.Bake'],
'helpers' => [
'Bake.Bake',
'Bake.DocBlock'
],
'theme' => $theme
];
$view = new BakeView(new Request(), new Response(), null, $viewOptions);
Expand Down
65 changes: 65 additions & 0 deletions src/Shell/Task/ModelTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public function bake($name)

$primaryKey = $this->getPrimaryKey($model);
$displayField = $this->getDisplayField($model);
$propertySchema = $this->getEntityPropertySchema($model);
$fields = $this->getFields();
$validation = $this->getValidation($model, $associations);
$rulesChecker = $this->getRules($model, $associations);
Expand All @@ -123,6 +124,7 @@ public function bake($name)
'primaryKey',
'displayField',
'table',
'propertySchema',
'fields',
'validation',
'rulesChecker',
Expand Down Expand Up @@ -424,6 +426,69 @@ public function getPrimaryKey($model)
return (array)$model->primaryKey();
}

/**
* Returns an entity property "schema".
*
* The schema is an associative array, using the property names
* as keys, and information about the property as the value.
*
* The value part consists of at least two keys:
*
* - `kind`: The kind of property, either `column`, which indicates
* that the property stems from a database column, or `association`,
* which identifies a property that is generated for an associated
* table.
* - `type`: The type of the property value. For the `column` kind
* this is the database type associated with the column, and for the
* `association` type it's the FQN of the entity class for the
* associated table.
*
* For `association` properties an additional key will be available
*
* - `association`: Holds an instance of the corresponding association
* class.
*
* @param \Cake\ORM\Table $model The model to introspect.
* @return array The property schema
*/
public function getEntityPropertySchema(Table $model)
{
$properties = [];

$schema = $model->schema();
foreach ($schema->columns() as $column) {
$properties[$column] = [
'kind' => 'column',
'type' => $schema->columnType($column)
];
}

foreach ($model->associations() as $association) {
$entityClass = '\\' . ltrim($association->target()->entityClass(), '\\');

if ($entityClass === '\Cake\ORM\Entity') {
$namespace = Configure::read('App.namespace');

list($plugin, ) = pluginSplit($association->target()->registryAlias());
if ($plugin !== null) {
$namespace = $plugin;
}
$namespace = str_replace('/', '\\', trim($namespace, '\\'));

$entityClass = $this->_entityName($association->target()->alias());
$entityClass = '\\' . $namespace . '\Model\Entity\\' . $entityClass;
}

$properties[$association->property()] = [
'kind' => 'association',
'association' => $association,
'type' => $entityClass
];
}

return $properties;
}

/**
* Evaluates the fields and no-fields options, and
* returns if, and which fields should be made accessible.
Expand Down
15 changes: 15 additions & 0 deletions src/Template/Bake/Model/entity.ctp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/

$propertyHintMap = null;
if (!empty($propertySchema)) {
$propertyHintMap = $this->DocBlock->buildEntityPropertyHintTypeMap($propertySchema);
}

$accessible = [];
if (!isset($fields) || $fields !== false) {
if (!empty($fields)) {
Expand All @@ -34,6 +39,16 @@ use Cake\ORM\Entity;

/**
* <%= $name %> Entity.
<% if ($propertyHintMap): %>
*
<% foreach ($propertyHintMap as $property => $type): %>
<% if ($type): %>
* @property <%= $type %> $<%= $property %>
<% else: %>
* @property $<%= $property %>
<% endif; %>
<% endforeach; %>
<% endif; %>
*/
class <%= $name %> extends Entity
{
Expand Down
143 changes: 143 additions & 0 deletions src/View/Helper/DocBlockHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
namespace Bake\View\Helper;

use Cake\ORM\Association;
use Cake\View\Helper;

/**
* DocBlock helper
*/
class DocBlockHelper extends Helper
{
/**
* Converts an entity class type to its DocBlock hint type counterpart.
*
* @param string $type The entity class type (a fully qualified class name).
* @param \Cake\ORM\Association $association The association related to the entity class.
* @return string The DocBlock type
*/
public function associatedEntityTypeToHintType($type, Association $association)
{
if ($association->type() === Association::MANY_TO_MANY ||
$association->type() === Association::ONE_TO_MANY
) {
return $type . '[]';
}
return $type;
}

/**
* Builds a map of entity property names and DocBlock types for use
* in generating `@property` hints.
*
* This method expects a property schema as generated by
* `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`.
*
* The generated map has the format of
*
* ```
* [
* 'property-name' => 'doc-block-type',
* ...
* ]
* ```
*
* @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema
*
* @param array $propertySchema The property schema to use for generating the type map.
* @return array The property DocType map.
*/
public function buildEntityPropertyHintTypeMap(array $propertySchema)
{
$properties = [];
foreach ($propertySchema as $property => $info) {
switch ($info['kind']) {
case 'column':
$properties[$property] = $this->columnTypeToHintType($info['type']);
break;

case 'association':
$type = $this->associatedEntityTypeToHintType($info['type'], $info['association']);
if ($info['association']->type() === Association::MANY_TO_ONE) {
$properties = $this->_insertAfter(
$properties,
$info['association']->foreignKey(),
[$property => $type]
);
} else {
$properties[$property] = $type;
}
break;
}
}
return $properties;
}

/**
* Converts a column type to its DocBlock type counterpart.
*
* This method only supports the default CakePHP column types,
* custom column/database types will be ignored.
*
* @see \Cake\Database\Type
*
* @param string $type The column type.
* @return null|string The DocBlock type, or `null` for unsupported column types.
*/
public function columnTypeToHintType($type)
{
switch ($type) {
case 'string':
case 'text':
case 'uuid':
return 'string';

case 'integer':
case 'biginteger':
return 'int';

case 'float':
case 'decimal':
return 'float';

case 'boolean':
return 'bool';

case 'binary':
return 'string|resource';

case 'date':
case 'datetime':
case 'time':
case 'timestamp':
return '\Cake\I18n\Time';
}

return null;
}

/**
* Inserts a value after a specific key in an associative array.
*
* In case the given key cannot be found, the value will be appended.
*
* @param array $target The array in which to insert the new value.
* @param string $key The array key after which to insert the new value.
* @param mixed $value The entry to insert.
* @return array The array with the new value inserted.
*/
protected function _insertAfter(array $target, $key, $value)
{
$index = array_search($key, array_keys($target));
if ($index !== false) {
$target = array_merge(
array_slice($target, 0, $index + 1),
$value,
array_slice($target, $index + 1, null)
);
} else {
$target += (array)$value;
}
return $target;
}
}
80 changes: 80 additions & 0 deletions tests/TestCase/Shell/Task/ModelTaskTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,63 @@ public function testHasAndBelongsToManyGeneration()
$this->assertEquals($expected, $result);
}

/**
* Test getting the entity property schema.
*
* @return void
*/
public function testGetEntityPropertySchema()
{
$model = TableRegistry::get('BakeArticles');
$model->belongsTo('BakeUsers');
$model->hasMany('BakeTest.Authors');
$model->schema()->columnType('created', 'timestamp');
$model->schema()->columnType('updated', 'timestamp');

$result = $this->Task->getEntityPropertySchema($model);
$expected = [
'id' => [
'kind' => 'column',
'type' => 'integer'
],
'title' => [
'kind' => 'column',
'type' => 'string'
],
'body' => [
'kind' => 'column',
'type' => 'text'
],
'created' => [
'kind' => 'column',
'type' => 'timestamp'
],
'bake_user_id' => [
'kind' => 'column',
'type' => 'integer'
],
'published' => [
'kind' => 'column',
'type' => 'boolean'
],
'updated' => [
'kind' => 'column',
'type' => 'timestamp'
],
'bake_user' => [
'kind' => 'association',
'association' => $model->association('BakeUsers'),
'type' => '\App\Model\Entity\BakeUser'
],
'authors' => [
'kind' => 'association',
'association' => $model->association('Authors'),
'type' => '\BakeTest\Model\Entity\Author'
]
];
$this->assertEquals($expected, $result);
}

/**
* Test getting accessible fields.
*
Expand Down Expand Up @@ -963,6 +1020,29 @@ public function testBakeEntity()
$this->assertSameAsFile(__FUNCTION__ . '.php', $result);
}

/**
* test baking an entity with DocBlock property type hints.
*
* @return void
*/
public function testBakeEntityWithPropertyTypeHints()
{
$model = TableRegistry::get('BakeArticles');
$model->belongsTo('BakeUsers');
$model->hasMany('BakeTest.Authors');
$model->schema()->addColumn('unknown_type', [
'type' => 'unknownType'
]);

$config = [
'fields' => false,
'propertySchema' => $this->Task->getEntityPropertySchema($model)
];

$result = $this->Task->bakeEntity($model, $config);
$this->assertSameAsFile(__FUNCTION__ . '.php', $result);
}

/**
* test baking an entity class
*
Expand Down
23 changes: 23 additions & 0 deletions tests/comparisons/Model/testBakeEntityWithPropertyTypeHints.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
* BakeArticle Entity.
*
* @property int $id
* @property int $bake_user_id
* @property \App\Model\Entity\BakeUser $bake_user
* @property string $title
* @property string $body
* @property bool $published
* @property \Cake\I18n\Time $created
* @property \Cake\I18n\Time $updated
* @property $unknown_type
* @property \BakeTest\Model\Entity\Author[] $authors
*/
class BakeArticle extends Entity
{

}

0 comments on commit 3b03927

Please sign in to comment.