Commit 51b04828 by Paul Klimov

Merge pull request #3964 from klimov-paul/sphinx-gii

Enh: Sphinx Gii generator created
parents 194f451a 7e823f09
......@@ -17,6 +17,7 @@ Yii Framework 2 sphinx extension Change Log
- Enh #1398: Refactor ActiveRecord to use BaseActiveRecord class of the framework (klimov-paul)
- Enh #2002: Added filterWhere() method to yii\spinx\Query to allow easy addition of search filter conditions by ignoring empty search fields (samdark, cebe)
- Enh #2892: ActiveRecord dirty attributes are now reset after call to `afterSave()` so information about changed attributes is available in `afterSave`-event (cebe)
- Enh #3964: Gii generator for Active Record model added (klimov-paul)
- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
- Chg #2146: Removed `ActiveRelation` class and moved the functionality to `ActiveQuery`.
All relational queries are now directly served by `ActiveQuery` allowing to use
......
......@@ -243,3 +243,27 @@ foreach ($articles as $article) {
echo $article->snippet;
}
```
Using Gii generator
-------------------
This extension provides a code generator, which can be integrated with yii 'gii' module. It allows generation of the
Active Record code. In order to enable it, you should adjust your application configuration in following way:
```php
return [
//....
'modules' => [
// ...
'gii' => [
'class' => 'yii\gii\Module',
'generators' => [
'sphinxModel' => [
'class' => 'yii\sphinx\gii\model\Generator'
]
],
],
]
];
```
\ No newline at end of file
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx\gii\model;
use Yii;
use yii\sphinx\ActiveRecord;
use yii\sphinx\Connection;
use yii\sphinx\Schema;
use yii\gii\CodeFile;
use yii\helpers\Inflector;
/**
* This generator will generate one or multiple ActiveRecord classes for the specified Sphinx index.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class Generator extends \yii\gii\Generator
{
public $db = 'sphinx';
public $ns = 'app\models';
public $indexName;
public $modelClass;
public $baseClass = 'yii\sphinx\ActiveRecord';
public $useIndexPrefix = false;
/**
* @inheritdoc
*/
public function getName()
{
return 'Sphinx Model Generator';
}
/**
* @inheritdoc
*/
public function getDescription()
{
return 'This generator generates an ActiveRecord class for the specified Sphinx index.';
}
/**
* @inheritdoc
*/
public function rules()
{
return array_merge(parent::rules(), [
[['db', 'ns', 'indexName', 'modelClass', 'baseClass'], 'filter', 'filter' => 'trim'],
[['ns'], 'filter', 'filter' => function($value) { return trim($value, '\\'); }],
[['db', 'ns', 'indexName', 'baseClass'], 'required'],
[['db', 'modelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
[['ns', 'baseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
[['indexName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'],
[['db'], 'validateDb'],
[['ns'], 'validateNamespace'],
[['indexName'], 'validateIndexName'],
[['modelClass'], 'validateModelClass', 'skipOnEmpty' => false],
[['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],
[['enableI18N'], 'boolean'],
[['useIndexPrefix'], 'boolean'],
[['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false],
]);
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return array_merge(parent::attributeLabels(), [
'ns' => 'Namespace',
'db' => 'Sphinx Connection ID',
'indexName' => 'Index Name',
'modelClass' => 'Model Class',
'baseClass' => 'Base Class',
]);
}
/**
* @inheritdoc
*/
public function hints()
{
return array_merge(parent::hints(), [
'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>',
'db' => 'This is the ID of the Sphinx application component.',
'indexName' => 'This is the name of the Sphinx index that the new ActiveRecord class is associated with, e.g. <code>post</code>.
The index name may end with asterisk to match multiple table names, e.g. <code>idx_*</code>
will match indexes, which name starts with <code>idx_</code>. In this case, multiple ActiveRecord classes
will be generated, one for each matching index name; and the class names will be generated from
the matching characters. For example, index <code>idx_post</code> will generate <code>Post</code>
class.',
'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
the namespace part as it is specified in "Namespace". You do not need to specify the class name
if "Index Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.',
'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
'useIndexPrefix' => 'This indicates whether the index name returned by the generated ActiveRecord class
should consider the <code>tablePrefix</code> setting of the Sphinx connection. For example, if the
index name is <code>idx_post</code> and <code>tablePrefix=idx_</code>, the ActiveRecord class
will return the table name as <code>{{%post}}</code>.',
]);
}
/**
* @inheritdoc
*/
public function autoCompleteData()
{
$db = $this->getDbConnection();
if ($db !== null) {
return [
'indexName' => function () use ($db) {
return $db->getSchema()->getIndexNames();
},
];
} else {
return [];
}
}
/**
* @inheritdoc
*/
public function requiredTemplates()
{
return ['model.php'];
}
/**
* @inheritdoc
*/
public function stickyAttributes()
{
return array_merge(parent::stickyAttributes(), ['ns', 'db', 'baseClass']);
}
/**
* @inheritdoc
*/
public function generate()
{
$files = [];
$db = $this->getDbConnection();
foreach ($this->getIndexNames() as $indexName) {
$className = $this->generateClassName($indexName);
$indexSchema = $db->getIndexSchema($indexName);
$params = [
'indexName' => $indexName,
'className' => $className,
'indexSchema' => $indexSchema,
'labels' => $this->generateLabels($indexSchema),
'rules' => $this->generateRules($indexSchema),
];
$files[] = new CodeFile(
Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $className . '.php',
$this->render('model.php', $params)
);
}
return $files;
}
/**
* Generates the attribute labels for the specified table.
* @param \yii\db\TableSchema $table the table schema
* @return array the generated attribute labels (name => label)
*/
public function generateLabels($table)
{
$labels = [];
foreach ($table->columns as $column) {
if (!strcasecmp($column->name, 'id')) {
$labels[$column->name] = 'ID';
} else {
$label = Inflector::camel2words($column->name);
if (strcasecmp(substr($label, -3), ' id') === 0) {
$label = substr($label, 0, -3) . ' ID';
}
$labels[$column->name] = $label;
}
}
return $labels;
}
/**
* Generates validation rules for the specified index.
* @param \yii\sphinx\IndexSchema $index the index schema
* @return array the generated validation rules
*/
public function generateRules($index)
{
$types = [];
foreach ($index->columns as $column) {
if ($column->isMva) {
$types['safe'][] = $column->name;
continue;
}
if ($column->isPrimaryKey) {
$types['required'][] = $column->name;
$types['unique'][] = $column->name;
}
switch ($column->type) {
case Schema::TYPE_PK:
case Schema::TYPE_INTEGER:
case Schema::TYPE_BIGINT:
$types['integer'][] = $column->name;
break;
case Schema::TYPE_BOOLEAN:
$types['boolean'][] = $column->name;
break;
case Schema::TYPE_FLOAT:
$types['number'][] = $column->name;
break;
case Schema::TYPE_TIMESTAMP:
$types['safe'][] = $column->name;
break;
default: // strings
$types['string'][] = $column->name;
}
}
$rules = [];
foreach ($types as $type => $columns) {
$rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
}
return $rules;
}
/**
* Validates the [[db]] attribute.
*/
public function validateDb()
{
if (!Yii::$app->has($this->db)) {
$this->addError('db', 'There is no application component named "' . $this->db . '".');
} elseif (!Yii::$app->get($this->db) instanceof Connection) {
$this->addError('db', 'The "' . $this->db . '" application component must be a Sphinx connection instance.');
}
}
/**
* Validates the [[ns]] attribute.
*/
public function validateNamespace()
{
$this->ns = ltrim($this->ns, '\\');
$path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
if ($path === false) {
$this->addError('ns', 'Namespace must be associated with an existing directory.');
}
}
/**
* Validates the [[modelClass]] attribute.
*/
public function validateModelClass()
{
if ($this->isReservedKeyword($this->modelClass)) {
$this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
}
if (substr($this->indexName, -1) !== '*' && $this->modelClass == '') {
$this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.');
}
}
/**
* Validates the [[indexName]] attribute.
*/
public function validateIndexName()
{
if (strpos($this->indexName, '*') !== false && substr($this->indexName, -1) !== '*') {
$this->addError('indexName', 'Asterisk is only allowed as the last character.');
return;
}
$tables = $this->getIndexNames();
if (empty($tables)) {
$this->addError('indexName', "Table '{$this->indexName}' does not exist.");
} else {
foreach ($tables as $table) {
$class = $this->generateClassName($table);
if ($this->isReservedKeyword($class)) {
$this->addError('indexName', "Table '$table' will generate a class which is a reserved PHP keyword.");
break;
}
}
}
}
private $_indexNames;
private $_classNames;
/**
* @return array the index names that match the pattern specified by [[indexName]].
*/
protected function getIndexNames()
{
if ($this->_indexNames !== null) {
return $this->_indexNames;
}
$db = $this->getDbConnection();
if ($db === null) {
return [];
}
$indexNames = [];
if (strpos($this->indexName, '*') !== false) {
$indexNames = $db->getSchema()->getIndexNames();
} elseif (($index = $db->getIndexSchema($this->indexName, true)) !== null) {
$indexNames[] = $this->indexName;
$this->_classNames[$this->indexName] = $this->modelClass;
}
return $this->_indexNames = $indexNames;
}
/**
* Generates the table name by considering table prefix.
* If [[useIndexPrefix]] is false, the table name will be returned without change.
* @param string $indexName the table name (which may contain schema prefix)
* @return string the generated table name
*/
public function generateIndexName($indexName)
{
if (!$this->useIndexPrefix) {
return $indexName;
}
$db = $this->getDbConnection();
if (preg_match("/^{$db->tablePrefix}(.*?)$/", $indexName, $matches)) {
$indexName = '{{%' . $matches[1] . '}}';
} elseif (preg_match("/^(.*?){$db->tablePrefix}$/", $indexName, $matches)) {
$indexName = '{{' . $matches[1] . '%}}';
}
return $indexName;
}
/**
* Generates a class name from the specified table name.
* @param string $indexName the table name (which may contain schema prefix)
* @return string the generated class name
*/
protected function generateClassName($indexName)
{
if (isset($this->_classNames[$indexName])) {
return $this->_classNames[$indexName];
}
if (($pos = strrpos($indexName, '.')) !== false) {
$indexName = substr($indexName, $pos + 1);
}
$db = $this->getDbConnection();
$patterns = [];
$patterns[] = "/^{$db->tablePrefix}(.*?)$/";
$patterns[] = "/^(.*?){$db->tablePrefix}$/";
if (strpos($this->indexName, '*') !== false) {
$pattern = $this->indexName;
if (($pos = strrpos($pattern, '.')) !== false) {
$pattern = substr($pattern, $pos + 1);
}
$patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
}
$className = $indexName;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $indexName, $matches)) {
$className = $matches[1];
break;
}
}
return $this->_classNames[$indexName] = Inflector::id2camel($className, '_');
}
/**
* @return Connection the Sphinx connection as specified by [[db]].
*/
protected function getDbConnection()
{
return Yii::$app->get($this->db, false);
}
}
<?php
/**
* This is the template for generating the model class of a specified Sphinx index.
*
* @var yii\web\View $this
* @var yii\sphinx\gii\model\Generator $generator
* @var string $indexName full table name
* @var string $className class name
* @var yii\sphinx\IndexSchema $indexSchema
* @var string[] $labels list of attribute labels (name => label)
* @var string[] $rules list of validation rules
*/
echo "<?php\n";
?>
namespace <?= $generator->ns ?>;
use Yii;
/**
* This is the model class for index "<?= $indexName ?>".
*
<?php foreach ($indexSchema->columns as $column): ?>
* @property <?= $column->isMva ? 'array' : $column->phpType ?> <?= "\${$column->name}\n" ?>
<?php endforeach; ?>
*/
class <?= $className ?> extends <?= '\\' . ltrim($generator->baseClass, '\\') . "\n" ?>
{
/**
* @inheritdoc
*/
public static function indexName()
{
return '<?= $generator->generateIndexName($indexName) ?>';
}
<?php if ($generator->db !== 'sphinx'): ?>
/**
* @return \yii\sphinx\Connection the database connection used by this AR class.
*/
public static function getDb()
{
return Yii::$app->get('<?= $generator->db ?>');
}
<?php endif; ?>
/**
* @inheritdoc
*/
public function rules()
{
return [<?= "\n " . implode(",\n ", $rules) . "\n " ?>];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
<?php foreach ($labels as $name => $label): ?>
<?= "'$name' => " . $generator->generateString($label) . ",\n" ?>
<?php endforeach; ?>
];
}
}
<?php
/**
* @var yii\web\View $this
* @var yii\widgets\ActiveForm $form
* @var yii\sphinx\gii\model\Generator $generator
*/
echo $form->field($generator, 'indexName');
echo $form->field($generator, 'modelClass');
echo $form->field($generator, 'ns');
echo $form->field($generator, 'baseClass');
echo $form->field($generator, 'db');
echo $form->field($generator, 'useIndexPrefix')->checkbox();
echo $form->field($generator, 'enableI18N')->checkbox();
echo $form->field($generator, 'messageCategory');
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment