From b85074a2bc954c5ba884caa26fb1060925ab4216 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 19 Jun 2014 13:23:33 +0300 Subject: [PATCH 1/2] Sphinx Gii generator created --- extensions/sphinx/gii/model/Generator.php | 389 ++++++++++++++++++++++++++ extensions/sphinx/gii/model/default/model.php | 67 +++++ extensions/sphinx/gii/model/form.php | 15 + 3 files changed, 471 insertions(+) create mode 100644 extensions/sphinx/gii/model/Generator.php create mode 100644 extensions/sphinx/gii/model/default/model.php create mode 100644 extensions/sphinx/gii/model/form.php diff --git a/extensions/sphinx/gii/model/Generator.php b/extensions/sphinx/gii/model/Generator.php new file mode 100644 index 0000000..07664db --- /dev/null +++ b/extensions/sphinx/gii/model/Generator.php @@ -0,0 +1,389 @@ + + * @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., app\models', + '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. post. + The index name may end with asterisk to match multiple table names, e.g. idx_* + will match indexes, which name starts with idx_. 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 idx_post will generate Post + 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 tablePrefix setting of the Sphinx connection. For example, if the + index name is idx_post and tablePrefix=idx_, the ActiveRecord class + will return the table name as {{%post}}.', + ]); + } + + /** + * @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); + } +} diff --git a/extensions/sphinx/gii/model/default/model.php b/extensions/sphinx/gii/model/default/model.php new file mode 100644 index 0000000..ae39d39 --- /dev/null +++ b/extensions/sphinx/gii/model/default/model.php @@ -0,0 +1,67 @@ + label) + * @var string[] $rules list of validation rules + */ + +echo " + +namespace ns ?>; + +use Yii; + +/** + * This is the model class for index "". + * +columns as $column): ?> + * @property isMva ? 'array' : $column->phpType ?> name}\n" ?> + + */ +class extends baseClass, '\\') . "\n" ?> +{ + /** + * @inheritdoc + */ + public static function indexName() + { + return 'generateIndexName($indexName) ?>'; + } +db !== 'sphinx'): ?> + + /** + * @return \yii\sphinx\Connection the database connection used by this AR class. + */ + public static function getDb() + { + return Yii::$app->get('db ?>'); + } + + + /** + * @inheritdoc + */ + public function rules() + { + return []; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + $label): ?> + " . $generator->generateString($label) . ",\n" ?> + + ]; + } +} diff --git a/extensions/sphinx/gii/model/form.php b/extensions/sphinx/gii/model/form.php new file mode 100644 index 0000000..1457664 --- /dev/null +++ b/extensions/sphinx/gii/model/form.php @@ -0,0 +1,15 @@ +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'); From 7e823f0972216cf4e6cac4980689c386dab9ce0d Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 19 Jun 2014 13:27:54 +0300 Subject: [PATCH 2/2] Docs about Sphinx Gii generator added --- extensions/sphinx/CHANGELOG.md | 1 + extensions/sphinx/README.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/extensions/sphinx/CHANGELOG.md b/extensions/sphinx/CHANGELOG.md index 9ed8ad2..b5f0b15 100644 --- a/extensions/sphinx/CHANGELOG.md +++ b/extensions/sphinx/CHANGELOG.md @@ -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 diff --git a/extensions/sphinx/README.md b/extensions/sphinx/README.md index e0c7481..4480723 100644 --- a/extensions/sphinx/README.md +++ b/extensions/sphinx/README.md @@ -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