No Description

ModelsCommand.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <?php
  2. /**
  3. * Laravel IDE Helper Generator
  4. *
  5. * @author Barry vd. Heuvel <barryvdh@gmail.com>
  6. * @copyright 2014 Barry vd. Heuvel / Fruitcake Studio (http://www.fruitcakestudio.nl)
  7. * @license http://www.opensource.org/licenses/mit-license.php MIT
  8. * @link https://github.com/barryvdh/laravel-ide-helper
  9. */
  10. namespace Barryvdh\LaravelIdeHelper\Console;
  11. use Illuminate\Console\Command;
  12. use Illuminate\Support\Str;
  13. use Symfony\Component\Console\Input\InputOption;
  14. use Symfony\Component\Console\Input\InputArgument;
  15. use Symfony\Component\ClassLoader\ClassMapGenerator;
  16. use phpDocumentor\Reflection\DocBlock;
  17. use phpDocumentor\Reflection\DocBlock\Context;
  18. use phpDocumentor\Reflection\DocBlock\Tag;
  19. use phpDocumentor\Reflection\DocBlock\Serializer as DocBlockSerializer;
  20. /**
  21. * A command to generate autocomplete information for your IDE
  22. *
  23. * @author Barry vd. Heuvel <barryvdh@gmail.com>
  24. */
  25. class ModelsCommand extends Command
  26. {
  27. /**
  28. * The console command name.
  29. *
  30. * @var string
  31. */
  32. protected $name = 'ide-helper:models';
  33. protected $filename = '_ide_helper_models.php';
  34. /**
  35. * The console command description.
  36. *
  37. * @var string
  38. */
  39. protected $description = 'Generate autocompletion for models';
  40. protected $properties = array();
  41. protected $methods = array();
  42. protected $write = false;
  43. protected $dirs = array();
  44. protected $reset;
  45. /**
  46. * Execute the console command.
  47. *
  48. * @return void
  49. */
  50. public function fire()
  51. {
  52. $filename = $this->option('filename');
  53. $this->write = $this->option('write');
  54. $this->dirs = array_merge(
  55. $this->laravel['config']->get('laravel-ide-helper::model_locations'),
  56. $this->option('dir')
  57. );
  58. $model = $this->argument('model');
  59. $ignore = $this->option('ignore');
  60. $this->reset = $this->option('reset');
  61. //If filename is default and Write is not specified, ask what to do
  62. if (!$this->write && $filename === $this->filename && !$this->option('nowrite')) {
  63. if ($this->confirm(
  64. "Do you want to overwrite the existing model files? Choose no to write to $filename instead? (Yes/No): "
  65. )
  66. ) {
  67. $this->write = true;
  68. }
  69. }
  70. $content = $this->generateDocs($model, $ignore);
  71. if (!$this->write) {
  72. $written = \File::put($filename, $content);
  73. if ($written !== false) {
  74. $this->info("Model information was written to $filename");
  75. } else {
  76. $this->error("Failed to write model information to $filename");
  77. }
  78. }
  79. }
  80. /**
  81. * Get the console command arguments.
  82. *
  83. * @return array
  84. */
  85. protected function getArguments()
  86. {
  87. return array(
  88. array('model', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Which models to include', array()),
  89. );
  90. }
  91. /**
  92. * Get the console command options.
  93. *
  94. * @return array
  95. */
  96. protected function getOptions()
  97. {
  98. return array(
  99. array('filename', 'F', InputOption::VALUE_OPTIONAL, 'The path to the helper file', $this->filename),
  100. array('dir', 'D', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The model dir', array()),
  101. array('write', 'W', InputOption::VALUE_NONE, 'Write to Model file'),
  102. array('nowrite', 'N', InputOption::VALUE_NONE, 'Don\'t write to Model file'),
  103. array('reset', 'R', InputOption::VALUE_NONE, 'Remove the original phpdocs instead of appending'),
  104. array('ignore', 'I', InputOption::VALUE_OPTIONAL, 'Which models to ignore', ''),
  105. );
  106. }
  107. protected function generateDocs($loadModels, $ignore = '')
  108. {
  109. $output = "<?php
  110. /**
  111. * An helper file for your Eloquent Models
  112. * Copy the phpDocs from this file to the correct Model,
  113. * And remove them from this file, to prevent double declarations.
  114. *
  115. * @author Barry vd. Heuvel <barryvdh@gmail.com>
  116. */
  117. \n\n";
  118. $hasDoctrine = interface_exists('Doctrine\DBAL\Driver');
  119. if (empty($loadModels)) {
  120. $models = $this->loadModels();
  121. } else {
  122. $models = array();
  123. foreach ($loadModels as $model) {
  124. $models = array_merge($models, explode(',', $model));
  125. }
  126. }
  127. $ignore = explode(',', $ignore);
  128. foreach ($models as $name) {
  129. if (in_array($name, $ignore)) {
  130. $this->comment("Ignoring model '$name'");
  131. continue;
  132. } else {
  133. $this->comment("Loading model '$name'");
  134. }
  135. $this->properties = array();
  136. $this->methods = array();
  137. if (class_exists($name)) {
  138. try {
  139. // handle abstract classes, interfaces, ...
  140. $reflectionClass = new \ReflectionClass($name);
  141. if (!$reflectionClass->IsInstantiable()) {
  142. throw new \Exception($name . ' is not instanciable.');
  143. } elseif (!$reflectionClass->isSubclassOf('Illuminate\Database\Eloquent\Model')) {
  144. $this->comment("Class '$name' is not a model");
  145. continue;
  146. }
  147. $model = new $name();
  148. if ($hasDoctrine) {
  149. $this->getPropertiesFromTable($model);
  150. }
  151. $this->getPropertiesFromMethods($model);
  152. $output .= $this->createPhpDocs($name);
  153. } catch (\Exception $e) {
  154. $this->error("Exception: " . $e->getMessage() . "\nCould not analyze class $name.");
  155. }
  156. } else {
  157. $this->error("Class $name does not exist");
  158. }
  159. }
  160. if (!$hasDoctrine) {
  161. $this->error(
  162. "Warning: 'doctrine/dbal: ~2.3' is required to load database information. Please require that in your composer.json and run 'composer update'."
  163. );
  164. }
  165. return $output;
  166. }
  167. protected function loadModels()
  168. {
  169. $models = array();
  170. foreach ($this->dirs as $dir) {
  171. $dir = base_path() . '/' . $dir;
  172. if (file_exists($dir)) {
  173. foreach (ClassMapGenerator::createMap($dir) as $model => $path) {
  174. $models[] = $model;
  175. }
  176. }
  177. }
  178. return $models;
  179. }
  180. /**
  181. * Load the properties from the database table.
  182. *
  183. * @param \Illuminate\Database\Eloquent\Model $model
  184. */
  185. protected function getPropertiesFromTable($model)
  186. {
  187. $table = $model->getConnection()->getTablePrefix() . $model->getTable();
  188. $schema = $model->getConnection()->getDoctrineSchemaManager($table);
  189. $schema->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
  190. $columns = $schema->listTableColumns($table);
  191. if ($columns) {
  192. foreach ($columns as $column) {
  193. $name = $column->getName();
  194. if (in_array($name, $model->getDates())) {
  195. $type = '\Carbon\Carbon';
  196. } else {
  197. $type = $column->getType()->getName();
  198. switch ($type) {
  199. case 'string':
  200. case 'text':
  201. case 'date':
  202. case 'time':
  203. case 'guid':
  204. case 'datetimetz':
  205. case 'datetime':
  206. $type = 'string';
  207. break;
  208. case 'integer':
  209. case 'bigint':
  210. case 'smallint':
  211. $type = 'integer';
  212. break;
  213. case 'decimal':
  214. case 'float':
  215. $type = 'float';
  216. break;
  217. case 'boolean':
  218. $type = 'boolean';
  219. break;
  220. default:
  221. $type = 'mixed';
  222. break;
  223. }
  224. }
  225. $this->setProperty($name, $type, true, true);
  226. $this->setMethod(
  227. Str::camel("where_" . $name),
  228. '\Illuminate\Database\Query\Builder|\\' . get_class($model),
  229. array('$value')
  230. );
  231. }
  232. }
  233. }
  234. /**
  235. * @param \Illuminate\Database\Eloquent\Model $model
  236. */
  237. protected function getPropertiesFromMethods($model)
  238. {
  239. $methods = get_class_methods($model);
  240. if ($methods) {
  241. foreach ($methods as $method) {
  242. if (Str::startsWith($method, 'get') && Str::endsWith(
  243. $method,
  244. 'Attribute'
  245. ) && $method !== 'getAttribute'
  246. ) {
  247. //Magic get<name>Attribute
  248. $name = Str::snake(substr($method, 3, -9));
  249. if (!empty($name)) {
  250. $this->setProperty($name, null, true, null);
  251. }
  252. } elseif (Str::startsWith($method, 'set') && Str::endsWith(
  253. $method,
  254. 'Attribute'
  255. ) && $method !== 'setAttribute'
  256. ) {
  257. //Magic set<name>Attribute
  258. $name = Str::snake(substr($method, 3, -9));
  259. if (!empty($name)) {
  260. $this->setProperty($name, null, null, true);
  261. }
  262. } elseif (Str::startsWith($method, 'scope') && $method !== 'scopeQuery') {
  263. //Magic set<name>Attribute
  264. $name = Str::camel(substr($method, 5));
  265. if (!empty($name)) {
  266. $reflection = new \ReflectionMethod($model, $method);
  267. $args = $this->getParameters($reflection);
  268. //Remove the first ($query) argument
  269. array_shift($args);
  270. $this->setMethod($name, '\\' . $reflection->class, $args);
  271. }
  272. } elseif (!method_exists('Eloquent', $method) && !Str::startsWith($method, 'get')) {
  273. //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
  274. $reflection = new \ReflectionMethod($model, $method);
  275. $file = new \SplFileObject($reflection->getFileName());
  276. $file->seek($reflection->getStartLine() - 1);
  277. $code = '';
  278. while ($file->key() < $reflection->getEndLine()) {
  279. $code .= $file->current();
  280. $file->next();
  281. }
  282. $begin = strpos($code, 'function(');
  283. $code = substr($code, $begin, strrpos($code, '}') - $begin + 1);
  284. foreach (array(
  285. 'hasMany',
  286. 'belongsToMany',
  287. 'hasOne',
  288. 'belongsTo',
  289. 'morphTo',
  290. 'morphMany',
  291. 'morphToMany'
  292. ) as $relation) {
  293. $search = '$this->' . $relation . '(';
  294. if ($pos = stripos($code, $search)) {
  295. $code = substr($code, $pos + strlen($search));
  296. $arguments = explode(',', substr($code, 0, stripos($code, ')')));
  297. //Remove quotes, ensure 1 \ in front of the model
  298. $returnModel = "\\" . ltrim(trim($arguments[0], " \"'"), "\\");
  299. if ($relation === "belongsToMany" or $relation === 'hasMany' or $relation === 'morphMany' or $relation === 'morphToMany') {
  300. //Collection or array of models (because Collection is Arrayable)
  301. $this->setProperty(
  302. $method,
  303. '\Illuminate\Database\Eloquent\Collection|' . $returnModel . '[]',
  304. true,
  305. null
  306. );
  307. } else {
  308. //Single model is returned
  309. $this->setProperty($method, $returnModel, true, null);
  310. }
  311. }
  312. }
  313. }
  314. }
  315. }
  316. }
  317. /**
  318. * @param string $name
  319. * @param string|null $type
  320. * @param bool|null $read
  321. * @param bool|null $write
  322. */
  323. protected function setProperty($name, $type = null, $read = null, $write = null)
  324. {
  325. if (!isset($this->properties[$name])) {
  326. $this->properties[$name] = array();
  327. $this->properties[$name]['type'] = 'mixed';
  328. $this->properties[$name]['read'] = false;
  329. $this->properties[$name]['write'] = false;
  330. }
  331. if ($type !== null) {
  332. $this->properties[$name]['type'] = $type;
  333. }
  334. if ($read !== null) {
  335. $this->properties[$name]['read'] = $read;
  336. }
  337. if ($write !== null) {
  338. $this->properties[$name]['write'] = $write;
  339. }
  340. }
  341. protected function setMethod($name, $type = '', $arguments = array())
  342. {
  343. if (!isset($this->methods[$name])) {
  344. $this->methods[$name] = array();
  345. $this->methods[$name]['type'] = $type;
  346. $this->methods[$name]['arguments'] = $arguments;
  347. }
  348. }
  349. /**
  350. * @param string $class
  351. * @return string
  352. */
  353. protected function createPhpDocs($class)
  354. {
  355. $reflection = new \ReflectionClass($class);
  356. $namespace = $reflection->getNamespaceName();
  357. $classname = $reflection->getShortName();
  358. $originalDoc = $reflection->getDocComment();
  359. if ($this->reset) {
  360. $phpdoc = new DocBlock('', new Context($namespace));
  361. } else {
  362. $phpdoc = new DocBlock($reflection, new Context($namespace));
  363. }
  364. if (!$phpdoc->getText()) {
  365. $phpdoc->setText($class);
  366. }
  367. $properties = array();
  368. $methods = array();
  369. foreach ($phpdoc->getTags() as $tag) {
  370. $name = $tag->getName();
  371. if ($name == "property" || $name == "property-read" || $name == "property-write") {
  372. $properties[] = $tag->getVariableName();
  373. } elseif ($name == "method") {
  374. $methods[] = $tag->getMethodName();
  375. }
  376. }
  377. foreach ($this->properties as $name => $property) {
  378. $name = "\$$name";
  379. if (in_array($name, $properties)) {
  380. continue;
  381. }
  382. if ($property['read'] && $property['write']) {
  383. $attr = 'property';
  384. } elseif ($property['write']) {
  385. $attr = 'property-write';
  386. } else {
  387. $attr = 'property-read';
  388. }
  389. $tag = Tag::createInstance("@{$attr} {$property['type']} {$name}", $phpdoc);
  390. $phpdoc->appendTag($tag);
  391. }
  392. foreach ($this->methods as $name => $method) {
  393. if (in_array($name, $methods)) {
  394. continue;
  395. }
  396. $arguments = implode(', ', $method['arguments']);
  397. $tag = Tag::createInstance("@method static {$method['type']} {$name}({$arguments}) ", $phpdoc);
  398. $phpdoc->appendTag($tag);
  399. }
  400. $serializer = new DocBlockSerializer();
  401. $serializer->getDocComment($phpdoc);
  402. $docComment = $serializer->getDocComment($phpdoc);
  403. if ($this->write) {
  404. $filename = $reflection->getFileName();
  405. $contents = \File::get($filename);
  406. if ($originalDoc) {
  407. $contents = str_replace($originalDoc, $docComment, $contents);
  408. } else {
  409. $needle = "class {$classname}";
  410. $replace = "{$docComment}\nclass {$classname}";
  411. $pos = strpos($contents, $needle);
  412. if ($pos !== false) {
  413. $contents = substr_replace($contents, $replace, $pos, strlen($needle));
  414. }
  415. }
  416. if (\File::put($filename, $contents)) {
  417. $this->info('Written new phpDocBlock to ' . $filename);
  418. }
  419. }
  420. $output = "namespace {$namespace}{\n{$docComment}\n\tclass {$classname} {}\n}\n\n";
  421. return $output;
  422. }
  423. /**
  424. * Get the parameters and format them correctly
  425. *
  426. * @param $method
  427. * @return array
  428. */
  429. public function getParameters($method)
  430. {
  431. //Loop through the default values for paremeters, and make the correct output string
  432. $params = array();
  433. $paramsWithDefault = array();
  434. foreach ($method->getParameters() as $param) {
  435. $paramStr = '$' . $param->getName();
  436. $params[] = $paramStr;
  437. if ($param->isOptional()) {
  438. $default = $param->getDefaultValue();
  439. if (is_bool($default)) {
  440. $default = $default ? 'true' : 'false';
  441. } elseif (is_array($default)) {
  442. $default = 'array()';
  443. } elseif (is_null($default)) {
  444. $default = 'null';
  445. } elseif (is_int($default)) {
  446. //$default = $default;
  447. } else {
  448. $default = "'" . trim($default) . "'";
  449. }
  450. $paramStr .= " = $default";
  451. }
  452. $paramsWithDefault[] = $paramStr;
  453. }
  454. return $paramsWithDefault;
  455. }
  456. }