├── composer.json ├── LICENSE ├── README.md └── src └── Ekhaled └── Generators └── MySQL └── Model.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ekhaled/f3-cortex-model-generator", 3 | "description": "Generates F3 Cortex models by reverse engineering existing database schema", 4 | "keywords": ["mysql","schema","model", "model-generator", "f3", "cortex", "fatfree"], 5 | "homepage": "https://github.com/ekhaled/schema-parser-mysql", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=5.3.6", 9 | "ext-pdo" : "*", 10 | "ekhaled/schema-parser-mysql": ">=1.3" 11 | }, 12 | "autoload": { 13 | "classmap": ["src/"] 14 | }, 15 | "authors": [ 16 | { 17 | "name": "ekhaled", 18 | "email": "me.khaled@gmail.com" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Khaled Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F3 Cortex model generator 2 | Generates [F3 Cortex](https://github.com/ikkez/f3-cortex) models by reverse engineering existing database schema. 3 | 4 | **Currently only supports MySQL.** 5 | 6 | ## Installation 7 | Please add 8 | ```php 9 | "ekhaled/f3-cortex-model-generator": "1.0" 10 | ``` 11 | to your composer file. 12 | 13 | ## Usage 14 | Create an executable PHP file with the following contents 15 | ```php 16 | #!/usr/bin/env php 17 | 'path/to/output/folder/', 22 | 'DB' => array(), //DB connection params 23 | 'namespace' => 'Models\\Base', 24 | 'extends' => '\\Models\\Base', 25 | 'relationNamespace' => '\\Models\Base\\', 26 | 'template' => 'path/to/template/file', 27 | 'indentation' => array(), 28 | 'exclude' => array() 29 | ]; 30 | 31 | $generator = new \Ekhaled\Generators\MySQL\Model($config); 32 | $generator->generate(); 33 | ``` 34 | and, just run the file from the command line. 35 | 36 | ## Options 37 | - `output` - specifies the folder where models will be output to. 38 | - `DB` - an array in the following format ['host' => 'host.com', 'username' => '', 'password' => '', 'dbname' => 'name_of_database',] 39 | - `namespace` - Namespace of the generated models 40 | - `extends` - if you have a base model, you can make the generated model extend that model by specifying it here. 41 | - `relationNamespace` - Namespace of the connected classes that constitute relationships with a given model, usually it's the same as `namespace` 42 | - `template` - Path to file containing a custom template, if not specified a built-in template will be used. 43 | - `indentation` - an array that indicates what type of unit of indentation to be used on template generation followed by a starting level. 44 | For example: `['unit' => ' ', 'start_level' => 3]`. This will use 2 spaces as indentation starting at 6 spaces. 45 | This is applied to the array defined by the {{FIELDCONF}} template 46 | - `exclude_views` - Whether to generate models for Views too, defaults to _false_. 47 | - `exclude_connectors` - Whether to generate stub models for many-to-many connector tables, defaults to _false_. (Sometimes you might need these models to create db tables, for example for automated tests in test databases). 48 | - `exclude` - An array containing all tables that you would like to exclude while generating models. For example: `array('migrations')`. 49 | 50 | ## Custom templates 51 | A typical custom template would look like: 52 | ```php 53 | 'path/to/output/folder', 19 | 'DB' => array(), 20 | 'namespace' => 'Models\\Base', 21 | 'extends' => '\\Models\\Base', 22 | 'relationNamespace' => '\\Models\Base\\', 23 | 'template' => '', 24 | 'indentation' => array(), 25 | 'exclude_views' => false, 26 | 'exclude_connectors' => false, 27 | 'exclude' => array() 28 | ); 29 | 30 | foreach ($config as $key => $value) { 31 | //overwrite the default value of config item if it exists 32 | if (array_key_exists($key, $defaults)) { 33 | $defaults[$key] = $value; 34 | } 35 | } 36 | 37 | //store the config back into the class property 38 | $this->config = $defaults; 39 | 40 | clearstatcache(); 41 | try { 42 | $this->checkConditions(); 43 | } catch (RuntimeException $ex) { 44 | $message = $ex->getMessage(); 45 | $this->output($message, true); 46 | exit; 47 | } 48 | } 49 | 50 | public function generate() 51 | { 52 | try { 53 | $schema = $this->getSchema(); 54 | } catch (PDOException $ex) { 55 | $message = $ex->getMessage(); 56 | $this->output("Database connection failed with the message 57 | >> \"" . $message . "\" 58 | Please ensure database connection settings are correct.", true); 59 | exit; 60 | } 61 | 62 | $config = $this->config; 63 | 64 | foreach ($schema as $table) { 65 | if (!in_array($table['name'], $config['exclude'])) { 66 | if ($config['exclude_views'] && $table['type'] == 'VIEW') { 67 | continue; 68 | } 69 | if ($config['exclude_connectors'] && $table['is_connector_table']) { 70 | continue; 71 | } 72 | $className = $this->className($table['name']); 73 | $h = fopen($config['output'] . $className . '.php', 'w'); 74 | if (fwrite($h, $this->generateModel( 75 | $table, 76 | $config['namespace'], 77 | $config['extends'], 78 | $config['relationNamespace'] 79 | ))) { 80 | $this->output("Generated " . $className . " model"); 81 | } else { 82 | $this->output('Failed to generate ' . $className . ' model', true); 83 | } 84 | 85 | fclose($h); 86 | usleep(250000); 87 | } 88 | } 89 | } 90 | 91 | protected function checkConditions() 92 | { 93 | $config = $this->config; 94 | 95 | if (!class_exists('\Ekhaled\MysqlSchema\Parser')) { 96 | throw new RuntimeException('This generator depends on ekhaled/schema-parser-mysql, please ensure that package is loaded'); 97 | } 98 | 99 | if (empty($config['output']) || !(file_exists($config['output']) && is_dir($config['output']) && is_writable($config['output']))) { 100 | throw new RuntimeException('Please ensure that the output folder exists and is writable.'); 101 | } 102 | 103 | if (!empty($this->config['template'])) { 104 | if (!(file_exists($this->config['template']) && is_file($this->config['template']))) { 105 | throw new RuntimeException("The specified template file does not exist.\nPlease leave the `template` option empty if you would like to use the built-in template."); 106 | } 107 | } 108 | } 109 | 110 | protected function generateModel($schema, $namespace = null, $extends = null, $relationNamespace = '', $classname = null) 111 | { 112 | $modelTemplate = $this->getTemplate(); 113 | 114 | $tablename = strtolower($schema['name']); 115 | 116 | $data = [ 117 | '{{NAMESPACE}}' => '', 118 | '{{CLASSNAME}}' => '', 119 | '{{EXTENDS}}' => '', 120 | '{{TABLENAME}}' => $tablename, 121 | ]; 122 | 123 | if ($namespace) { 124 | $data['{{NAMESPACE}}'] = 'namespace ' . $namespace . ';'; 125 | } 126 | 127 | if ($extends) { 128 | $data['{{EXTENDS}}'] = 'extends ' . $extends; 129 | } 130 | 131 | if (!$classname) { 132 | $classname = $this->className($tablename); 133 | } 134 | 135 | $data['{{CLASSNAME}}'] = $classname; 136 | 137 | $fieldConf = []; 138 | foreach ($schema['columns'] as $column) { 139 | if (!($column['isPrimaryKey'] && $column['autoIncrement'])) { 140 | $fieldConf[] = $this->field($column, $relationNamespace); 141 | } 142 | } 143 | 144 | foreach ($schema['relations'] as $rel) { 145 | $fieldConf[] = $this->virtualfield($rel, $relationNamespace); 146 | } 147 | 148 | $modelTemplate = str_replace(array_keys($data), array_values($data), $modelTemplate); 149 | $modelTemplate = str_replace('{{FIELDCONF}}', implode(",\n", $fieldConf), $modelTemplate); 150 | 151 | return $modelTemplate; 152 | } 153 | 154 | protected function getTemplate() 155 | { 156 | if (empty($this->_template)) { 157 | if (!empty($this->config['template'])) { 158 | $this->_template = file_get_contents($this->config['template']); 159 | } else { 160 | $this->_template = <<_template; 178 | } 179 | 180 | protected function getSchema() 181 | { 182 | $schemaParser = new \Ekhaled\MysqlSchema\Parser($this->config['DB']); 183 | return $schemaParser->getSchema(); 184 | } 185 | 186 | protected function className($t, $ns = '') 187 | { 188 | return $ns . ucfirst(strtolower($t)); 189 | } 190 | 191 | protected function field(array $field, $relationNamespace = '') 192 | { 193 | $indentConfig = $this->getIndentationConfig(); 194 | 195 | $template = $indentConfig['field_name_indent'] . '\'' . $field['name'] . '\' => [ 196 | {{VALUES}} 197 | ' . $indentConfig['field_name_indent'] . ']'; 198 | 199 | $values = []; 200 | 201 | if (isset($field['relation']) && count($field['relation']) > 0) { 202 | $values[] = '\'' . $field['relation']['type'] . '\' => \'' . $this->className($field['relation']['table'], $relationNamespace) . '\''; 203 | } else { 204 | 205 | $values[] = '\'type\' => \'' . $this->extractType($field['type']) . '\''; 206 | 207 | if (trim($field['default']) !== '') { 208 | $values[] = '\'default\' => \'' . $field['default'] . '\''; 209 | } 210 | 211 | $values[] = '\'nullable\' => ' . ($field['nullable'] ? 'true' : 'false'); 212 | } 213 | 214 | $template = str_replace('{{VALUES}}', implode(",\n", array_map(function ($val) use ($indentConfig) { 215 | return $indentConfig['values_indent'] . $val; 216 | }, $values)), $template); 217 | 218 | return $template; 219 | } 220 | 221 | protected function virtualfield(array $relation, $relationNamespace = '') 222 | { 223 | $indentConfig = $this->getIndentationConfig(); 224 | if (isset($relation['via'])) { 225 | return $indentConfig['field_name_indent'] . '\'' . $relation['selfColumn'] . '\' => [ 226 | ' . $indentConfig['values_indent'] . '\'' . $relation['type'] . '\' => [\'' . $this->className($relation['table'], $relationNamespace) . '\', \'' . $relation['column'] . '\', \'' . $relation['via'] . '\'] 227 | ' . $indentConfig['field_name_indent'] . ']'; 228 | } else { 229 | return $indentConfig['field_name_indent'] . '\'' . (isset($relation['key']) ? $relation['key'] : $relation['table']) . '\' => [ 230 | ' . $indentConfig['values_indent'] . '\'' . $relation['type'] . '\' => [\'' . $this->className($relation['table'], $relationNamespace) . '\', \'' . $relation['column'] . '\'] 231 | ' . $indentConfig['field_name_indent'] . ']'; 232 | } 233 | } 234 | 235 | protected function extractType($dbType) 236 | { 237 | 238 | $ints = [ 239 | 1 => 1, 240 | 2 => 1, 241 | 3 => 1, 242 | 4 => 1, 243 | 5 => 2, 244 | 6 => 2, 245 | 7 => 2, 246 | 8 => 4, 247 | 9 => 4, 248 | 10 => 4, 249 | 11 => 4, 250 | 20 => 8 251 | ]; 252 | $varchars = [128, 256, 512]; 253 | 254 | $size = null; 255 | if (strpos($dbType, '(') && preg_match('/\((.*)\)/', $dbType, $matches)) { 256 | $values = explode(',', $matches[1]); 257 | $size = $values[0]; 258 | } 259 | 260 | if (stripos($dbType, 'tinyint') !== false) 261 | $type = 'INT1'; 262 | elseif (stripos($dbType, 'int') !== false && stripos($dbType, 'unsigned int') === false) 263 | $type = 'INT'; 264 | elseif (stripos($dbType, 'longtext') !== false) 265 | $type = 'LONGTEXT'; 266 | elseif (stripos($dbType, 'text') !== false) 267 | $type = 'TEXT'; 268 | elseif (stripos($dbType, 'bool') !== false) 269 | $type = 'BOOLEAN'; 270 | elseif (stripos($dbType, 'datetime') !== false) 271 | $type = 'DATETIME'; 272 | elseif (stripos($dbType, 'date') !== false) 273 | $type = 'DATE'; 274 | elseif (stripos($dbType, 'timestamp') !== false) 275 | $type = 'TIMESTAMP'; 276 | elseif (preg_match('/(real|floa|doub)/i', $dbType)) 277 | $type = 'DOUBLE'; 278 | else 279 | $type = 'VARCHAR'; 280 | 281 | if ($type == 'VARCHAR' && in_array($size, $varchars)) { 282 | return $type . $size; 283 | } elseif ($type == 'VARCHAR') { 284 | return 'VARCHAR128'; 285 | } 286 | 287 | if ($type == 'INT') { 288 | if (in_array($size, array_keys($ints))) { 289 | return $type . $ints[$size]; 290 | } else { 291 | return $type . '8'; 292 | } 293 | } 294 | 295 | return $type; 296 | } 297 | 298 | protected function getIndentationConfig() 299 | { 300 | $indentation = $this->config['indentation']; 301 | $start_level = isset($indentation['start_level']) ? $indentation['start_level'] : 3; 302 | $unit = isset($indentation['unit']) ? $indentation['unit'] : ' '; 303 | 304 | 305 | $fieldNameIndent = str_repeat($unit, $start_level); 306 | $valuesIndent = $fieldNameIndent . $unit; 307 | 308 | return [ 309 | 'field_name_indent' => $fieldNameIndent, 310 | 'values_indent' => $valuesIndent 311 | ]; 312 | } 313 | 314 | protected function output($msg, $err = false) 315 | { 316 | if ($err) { 317 | echo "\033[1;97;41m" . $msg . "\e[0m" . "\n"; 318 | } else { 319 | echo "\033[1;97;42m" . $msg . "\e[0m" . "\n"; 320 | } 321 | } 322 | } 323 | --------------------------------------------------------------------------------