Skip to content

Commit 2716b3a

Browse files
committed
added runway RecordCommand to codebase
1 parent 94fd5cd commit 2716b3a

File tree

6 files changed

+640
-47
lines changed

6 files changed

+640
-47
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ test.db
55
.vscode/
66
coverage/
77
clover.xml
8-
.phpunit.result.cache
8+
.phpunit.result.cache
9+
.runway-config.json

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"phpunit/phpunit": "^9.0",
2727
"squizlabs/php_codesniffer": "^3.8",
2828
"rregeer/phpunit-coverage-check": "^0.3.1",
29-
"flightphp/runway": "^0.1.0"
29+
"flightphp/runway": "^0.2"
3030
},
3131
"autoload": {
3232
"psr-4": {"flight\\": "src/"}

src/ActiveRecord.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ public function reset(bool $include_query_data = true): self
299299
{
300300
$this->data = [];
301301
$this->customData = [];
302-
$this->isHydrated = false;
302+
$this->isHydrated = false;
303303
if ($include_query_data === true) {
304304
$this->resetQueryData();
305305
}
@@ -452,16 +452,16 @@ public function insert(): ActiveRecord
452452
]);
453453
$this->values = new Expressions(['operator' => 'VALUES', 'target' => new WrapExpressions(['target' => $value])]);
454454

455-
$intentionallyAssignedPrimaryKey = $this->dirty[$this->primaryKey] ?? null;
455+
$intentionallyAssignedPrimaryKey = $this->dirty[$this->primaryKey] ?? null;
456456

457457
$this->execute($this->buildSql(['insert', 'values']), $this->params);
458458

459459
$this->{$this->primaryKey} = $intentionallyAssignedPrimaryKey ?: $this->databaseConnection->lastInsertId();
460460

461461
$this->processEvent([ 'afterInsert', 'afterSave' ], [ $this ]);
462462

463-
$this->isHydrated = true;
464-
463+
$this->isHydrated = true;
464+
465465
return $this->dirty();
466466
}
467467
/**
@@ -470,16 +470,16 @@ public function insert(): ActiveRecord
470470
*/
471471
public function update(): ActiveRecord
472472
{
473-
$this->processEvent([ 'beforeUpdate', 'beforeSave' ], [ $this ]);
474-
473+
$this->processEvent([ 'beforeUpdate', 'beforeSave' ], [ $this ]);
474+
475475
foreach ($this->dirty as $field => $value) {
476476
$this->addCondition($field, '=', $value, ',', 'set');
477477
}
478478

479-
// Only update something if there is something to update
480-
if(count($this->dirty) > 0) {
481-
$this->execute($this->eq($this->primaryKey, $this->{$this->primaryKey})->buildSql(['update', 'set', 'where']), $this->params);
482-
}
479+
// Only update something if there is something to update
480+
if (count($this->dirty) > 0) {
481+
$this->execute($this->eq($this->primaryKey, $this->{$this->primaryKey})->buildSql(['update', 'set', 'where']), $this->params);
482+
}
483483

484484
$this->processEvent([ 'afterUpdate', 'afterSave' ], [ $this ]);
485485

src/commands/RecordCommand.php

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\commands;
6+
7+
use Nette\PhpGenerator\ClassType;
8+
use Nette\PhpGenerator\PhpFile;
9+
use Nette\PhpGenerator\PhpNamespace;
10+
11+
class RecordCommand extends AbstractBaseCommand
12+
{
13+
/**
14+
* Construct
15+
*
16+
* @param array<string,mixed> $config JSON config from .runway-config.json
17+
*/
18+
public function __construct(array $config)
19+
{
20+
parent::__construct('make:record', 'Creates a new Active Record based on the columns in your database table.', $config);
21+
$this->argument('<table_name>', 'The name of the table to read from and create the active record');
22+
$this->argument('[class_name]', 'The name of the active record class to create');
23+
$this->usage(<<<TEXT
24+
<bold> make:record users</end> ## Creates a file named UserRecord.php based on the users table<eol/>
25+
<bold> make:record users Author</end> ## Creates a file named AuthorRecord.php based on the users table<eol/>
26+
TEXT);
27+
}
28+
29+
/**
30+
* Executes the record command.
31+
*
32+
* @param string $tableName The name of the table to perform the command on.
33+
* @param string $className The name of the class to use for the record. (optional)
34+
* @return void
35+
* @
36+
*/
37+
public function execute(string $tableName, ?string $className = null)
38+
{
39+
$io = $this->app()->io();
40+
if (isset($this->config['app_root']) === false) {
41+
$io->error('app_root not set in .runway-config.json', true);
42+
return;
43+
}
44+
45+
if (isset($this->config['database']) === false) {
46+
$this->registerDatabaseConfig();
47+
}
48+
49+
if ($className === '' || $className === null) {
50+
$className = $this->singularizeTable($tableName);
51+
}
52+
53+
if (!preg_match('/Record$/', $className)) {
54+
$className .= 'Record';
55+
}
56+
57+
$recordPath = getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'records' . DIRECTORY_SEPARATOR . $className . '.php';
58+
if (file_exists($recordPath) === true) {
59+
$io->error($className . ' already exists.', true);
60+
return;
61+
}
62+
63+
if (is_dir(dirname($recordPath)) === false) {
64+
$io->info('Creating directory ' . dirname($recordPath), true);
65+
mkdir(dirname($recordPath), 0755, true);
66+
}
67+
68+
$file = new PhpFile();
69+
$file->setStrictTypes();
70+
71+
$namespace = new PhpNamespace('app\\records');
72+
73+
$class = new ClassType($className);
74+
$class->setExtends('flight\\ActiveRecord');
75+
76+
$pdo = $this->getPdoConnection();
77+
78+
// need to pull out all the fields from the table
79+
// for the various database drivers
80+
// this also will normalize the types to php types
81+
$fields = [];
82+
if ($this->config['database']['driver'] === 'mysql') {
83+
$statement = $pdo->query('DESCRIBE ' . $tableName);
84+
$rawFields = $statement->fetchAll(\PDO::FETCH_ASSOC);
85+
$fields = array_map(function ($field) {
86+
$type = $field['Type'];
87+
$phpType = $this->getPhpTypeFromDatabaseType($type);
88+
return [
89+
'name' => $field['Field'],
90+
'type' => $phpType
91+
];
92+
}, $rawFields);
93+
} elseif ($this->config['database']['driver'] === 'pgsql') {
94+
$statement = $pdo->query('SELECT column_name, data_type FROM information_schema.columns WHERE table_name = \'' . $tableName . '\'');
95+
$rawFields = $statement->fetchAll(\PDO::FETCH_ASSOC);
96+
$fields = array_map(function ($field) {
97+
$type = $field['data_type'];
98+
$phpType = $this->getPhpTypeFromDatabaseType($type);
99+
return [
100+
'name' => $field['column_name'],
101+
'type' => $phpType
102+
];
103+
}, $rawFields);
104+
} elseif ($this->config['database']['driver'] === 'sqlite') {
105+
$statement = $pdo->query('PRAGMA table_info(' . $tableName . ')');
106+
$rawFields = $statement->fetchAll(\PDO::FETCH_ASSOC);
107+
$fields = array_map(function ($field) {
108+
$type = $field['type'];
109+
$phpType = $this->getPhpTypeFromDatabaseType($type);
110+
return [
111+
'name' => $field['name'],
112+
'type' => $phpType
113+
];
114+
}, $rawFields);
115+
}
116+
117+
$class->addComment('ActiveRecord class for the ' . $tableName . ' table.');
118+
119+
$class->addComment('@link https://docs.flightphp.com/awesome-plugins/active-record');
120+
$class->addComment('');
121+
122+
foreach ($fields as $field) {
123+
$class->addComment('@property ' . $field['type'] . ' $' . $field['name']);
124+
}
125+
$class->addProperty('relations')
126+
->setVisibility('protected')
127+
->setType('array')
128+
->setValue([])
129+
->addComment('@var array $relations Set the relationships for the model' . "\n" . ' https://docs.flightphp.com/awesome-plugins/active-record#relationships');
130+
$method = $class->addMethod('__construct')
131+
->addComment('Constructor')
132+
->addComment('@param mixed $databaseConnection The connection to the database')
133+
->setVisibility('public')
134+
->setBody('parent::__construct($databaseConnection, \'' . $tableName . '\');');
135+
$method->addParameter('databaseConnection');
136+
137+
$namespace->add($class);
138+
$file->addNamespace($namespace);
139+
140+
$this->persistClass($className, $file);
141+
142+
$io->ok('Active Record successfully created at ' . $recordPath, true);
143+
}
144+
145+
/**
146+
* Saves the class name to a file
147+
*
148+
* @param string $recordName Name of the Controller
149+
* @param PhpFile $file Class Object from Nette\PhpGenerator
150+
*
151+
* @return void
152+
*/
153+
protected function persistClass(string $recordName, PhpFile $file)
154+
{
155+
$printer = new \Nette\PhpGenerator\PsrPrinter();
156+
file_put_contents(getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'records' . DIRECTORY_SEPARATOR . $recordName . '.php', $printer->printFile($file));
157+
}
158+
159+
/**
160+
* Does the setup for the database configuration
161+
*
162+
* @return void
163+
*/
164+
protected function registerDatabaseConfig()
165+
{
166+
$interactor = $this->app()->io();
167+
168+
$interactor->boldBlue('Database configuration not found. Please provide the following details:', true);
169+
170+
$driver = $interactor->choice('Driver', ['mysql', 'pgsql', 'sqlite'], 'mysql');
171+
172+
$file_path = '';
173+
$host = '';
174+
$port = '';
175+
$database = '';
176+
$charset = '';
177+
if ($driver === 'sqlite') {
178+
$file_path = $interactor->prompt('Database file path', 'database.sqlite');
179+
} else {
180+
$host = $interactor->prompt('Host', 'localhost');
181+
$port = $interactor->prompt('Port', '3306');
182+
$database = $interactor->prompt('Database');
183+
if ($driver === 'mysql') {
184+
$charset = $interactor->prompt('Charset', 'utf8mb4');
185+
}
186+
}
187+
188+
$username = $interactor->prompt('Username (for no username, press enter)', '', null, 0);
189+
$password = $interactor->prompt('Password (for no password, press enter)', '', null, 0);
190+
191+
$this->config['database'] = [
192+
'driver' => $driver,
193+
'host' => $host,
194+
'port' => $port,
195+
'database' => $database,
196+
'username' => $username,
197+
'password' => $password,
198+
'charset' => $charset,
199+
'file_path' => $file_path
200+
];
201+
202+
$interactor->info('Writing database configuration to .runway-config.json', true);
203+
file_put_contents(getcwd() . DIRECTORY_SEPARATOR . '.runway-config.json', json_encode($this->config, JSON_PRETTY_PRINT));
204+
}
205+
206+
/**
207+
* Gets the PDO connection
208+
*
209+
* @return \PDO
210+
*/
211+
protected function getPdoConnection(): \PDO
212+
{
213+
$database = $this->config['database'];
214+
if ($database['driver'] === 'sqlite') {
215+
$dsn = $database['driver'] . ':' . $database['file_path'];
216+
} else {
217+
// @codeCoverageIgnoreStart
218+
// This is due to only being able to test sqlite in unit test mode.
219+
$dsn = $database['driver'] . ':host=' . $database['host'] . ';port=' . $database['port'] . ';dbname=' . $database['database'];
220+
if ($database['driver'] === 'mysql') {
221+
$dsn .= ';charset=' . $database['charset'];
222+
}
223+
// @codeCoverageIgnoreEnd
224+
}
225+
return new \PDO($dsn, $database['username'], $database['password']);
226+
}
227+
228+
/**
229+
* Gets the PHP type from the database type
230+
*
231+
* @param string $type Database type
232+
*
233+
* @return string
234+
*/
235+
protected function getPhpTypeFromDatabaseType(string $type): string
236+
{
237+
$phpType = '';
238+
if (stripos($type, 'int') !== false) {
239+
$phpType = 'int';
240+
} elseif (stripos($type, 'float') !== false || stripos($type, 'double') !== false || stripos($type, 'decimal') !== false || stripos($type, 'numeric') !== false) {
241+
$phpType = 'float';
242+
} elseif (stripos($type, 'binary') !== false || stripos($type, 'blob') !== false || stripos($type, 'byte') !== false) {
243+
$phpType = 'mixed';
244+
} else {
245+
$phpType = 'string';
246+
}
247+
return $phpType;
248+
}
249+
250+
/**
251+
* Takes a table name, makes it singular (including tables that end in ses)
252+
* and then converts it from snake_case to CamelCase
253+
*
254+
* @param string $table [description]
255+
* @return string
256+
*/
257+
protected function singularizeTable(string $table): string
258+
{
259+
$className = $table;
260+
if (substr($table, -3) === 'ses') {
261+
$className = substr($table, 0, -2);
262+
} elseif (substr($table, -1) === 's') {
263+
$className = substr($table, 0, -1);
264+
}
265+
$className = str_replace('_', ' ', $className);
266+
$className = ucwords($className);
267+
$className = str_replace(' ', '', $className);
268+
return $className;
269+
}
270+
}

0 commit comments

Comments
 (0)