瀏覽代碼

Add upgrade tool

shalvah 4 年之前
父節點
當前提交
24cc667246
共有 4 個文件被更改,包括 475 次插入12 次删除
  1. 56 0
      src/Commands/Upgrade.php
  2. 0 1
      src/Extracting/Strategies/GetFromInlineValidatorBase.php
  3. 14 11
      src/ScribeServiceProvider.php
  4. 405 0
      src/Tools/Upgrader.php

+ 56 - 0
src/Commands/Upgrade.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Knuckles\Scribe\Commands;
+
+use Illuminate\Console\Command;
+use Knuckles\Scribe\Tools\Upgrader;
+
+class Upgrade extends Command
+{
+    protected $signature = "scribe:upgrade {version=v3} {--dry-run}";
+
+    protected $description = '';
+
+    public function handle(): void
+    {
+        $toVersion = $this->argument('version');
+        if ($toVersion !== 'v3') {
+            return;
+        }
+
+        $oldConfig = config('scribe');
+        $upgrader = Upgrader::ofConfigFile('config/scribe.php', __DIR__ . '/../../config/scribe.php')
+            ->dontTouch('routes')
+            ->move('interactive', 'try_it_out.enabled');
+
+        if ($this->option('dry-run')) {
+            $changes = $upgrader->dryRun();
+            if (empty($changes)) {
+                $this->info("No changes needed! Looks like you're all set.");
+                return;
+            }
+
+            $this->info('The following changes will be made to your config file:');
+            $this->newLine();
+            foreach ($changes as $change) {
+                $this->info($change["description"]);
+            }
+            return;
+        }
+
+        $upgrader->upgrade();
+
+        if (!empty($oldConfig["continue_without_database_transactions"])) {
+            $this->warn(
+                '`continue_without_database_transactions` was deprecated in 2.4.0. Your new config file now uses `database_connections_to_transact`.'
+                );
+        }
+
+        $this->newLine();
+        $this->info("✔ Upgraded your config to $toVersion. Your old config is backed up at config/scribe.php.bak.");
+        $this->info("Please review to catch any mistakes.");
+        $this->warn("If you have any custom strategies or views, you should migrate those manually. See the migration guide at http://scribe.knuckles.wtf.");
+        $this->info("Don't forget to check out the release announcement for new features!");
+    }
+
+}

+ 0 - 1
src/Extracting/Strategies/GetFromInlineValidatorBase.php

@@ -5,7 +5,6 @@ namespace Knuckles\Scribe\Extracting\Strategies;
 use Knuckles\Camel\Extraction\ExtractedEndpointData;
 use Knuckles\Scribe\Extracting\MethodAstParser;
 use Knuckles\Scribe\Extracting\ParsesValidationRules;
-use Knuckles\Scribe\Extracting\Strategies\Strategy;
 use PhpParser\Node;
 use PhpParser\Node\Stmt\ClassMethod;
 

+ 14 - 11
src/ScribeServiceProvider.php

@@ -5,6 +5,7 @@ namespace Knuckles\Scribe;
 use Illuminate\Support\ServiceProvider;
 use Knuckles\Scribe\Commands\GenerateDocumentation;
 use Knuckles\Scribe\Commands\MakeStrategy;
+use Knuckles\Scribe\Commands\Upgrade;
 use Knuckles\Scribe\Matching\RouteMatcher;
 use Knuckles\Scribe\Matching\RouteMatcherInterface;
 use Knuckles\Scribe\Tools\BladeMarkdownEngine;
@@ -30,17 +31,18 @@ class ScribeServiceProvider extends ServiceProvider
     {
         $this->loadViewsFrom(__DIR__ . '/../resources/views/', 'scribe');
 
-        $this->publishes([
-            __DIR__ . '/../resources/views' => $this->app->basePath('resources/views/vendor/scribe'),
-        ], 'scribe-views');
-
-        $this->publishes([
-            __DIR__ . '/../resources/views/partials/example-requests' => $this->app->basePath('resources/views/vendor/scribe/partials/example-requests'),
-        ], 'scribe-examples');
-
-        $this->publishes([
-            __DIR__ . '/../resources/views/themes' => $this->app->basePath('resources/views/vendor/scribe/themes'),
-        ], 'scribe-themes');
+        // Publish views in separate, smaller groups for ease of end-user modifications
+        $viewGroups = [
+            'views' => '',
+            'examples' => 'partials/example-requests',
+            'themes' => 'themes',
+            'markdown' => 'markdown',
+        ];
+        foreach ($viewGroups as $group => $path) {
+            $this->publishes([
+                __DIR__ . "/../resources/views/$path" => $this->app->basePath("resources/views/vendor/scribe/$path"),
+            ], "scribe-$group");
+        }
 
         $this->publishes([
             __DIR__ . '/../config/scribe.php' => $this->app->configPath('scribe.php'),
@@ -54,6 +56,7 @@ class ScribeServiceProvider extends ServiceProvider
             $this->commands([
                 GenerateDocumentation::class,
                 MakeStrategy::class,
+                Upgrade::class,
             ]);
         }
 

+ 405 - 0
src/Tools/Upgrader.php

@@ -0,0 +1,405 @@
+<?php
+
+
+namespace Knuckles\Scribe\Tools;
+
+
+use Illuminate\Support\Arr;
+use Illuminate\Support\Str;
+use PhpParser;
+use PhpParser\{Node, NodeFinder, Lexer, NodeTraverser, NodeVisitor, Parser, ParserFactory, PrettyPrinter};
+
+class Upgrader
+{
+    public const CHANGE_REMOVED = 'removed';
+    public const CHANGE_RENAMED = 'renamed';
+    public const CHANGE_ADDED = 'added';
+    public const CHANGE_ARRAY_ITEM_ADDED = 'added_to_array';
+
+    private array $configFiles = [];
+    private array $movedKeys = [];
+    private array $dontTouchKeys = [];
+    private array $userFacingChanges = [];
+    private array $catchUps = [];
+
+    /** @var Node\Stmt[] */
+    private ?array $outgoingConfigFileAst = [];
+    /** @var Node\Stmt[] */
+    private array $incomingConfigFileAstForModification = [];
+    /** @var Node\Stmt[]|null */
+    private ?array $incomingConfigFileOriginalAst;
+    private array $incomingConfigFileOriginalTokens;
+
+    public function __construct(string $userRelativeLocation, string $packageAbsolutePath)
+    {
+        $this->configFiles['user_relative'] = $userRelativeLocation;
+        $this->configFiles['package_absolute'] = $packageAbsolutePath;
+    }
+
+    public static function ofConfigFile(string $userRelativeLocation, string $packageAbsolutePath): self
+    {
+        return new self($userRelativeLocation, $packageAbsolutePath);
+    }
+
+    public function upgrade()
+    {
+        $this->fetchUserFacingChanges();
+        $this->applyChanges();
+    }
+
+    public function dryRun(): array
+    {
+        $this->fetchUserFacingChanges(true);
+        return $this->userFacingChanges;
+    }
+
+    protected function fetchUserFacingChanges($forDisplay = false)
+    {
+        $userCurrentConfig = require(getcwd() . '/' . ltrim($this->configFiles['user_relative'], '/'));
+        $incomingConfig = require $this->configFiles['package_absolute'];
+
+        $forDisplay && $this->fetchAddedItems($userCurrentConfig, $incomingConfig);
+        $this->fetchRemovedOrRenamedItems($userCurrentConfig, $incomingConfig);
+    }
+
+    protected function fetchRemovedOrRenamedItems(array $userCurrentConfig, $incomingConfig, string $rootKey = '')
+    {
+
+        if (is_array($incomingConfig)) {
+            $arrayKeys = array_keys($incomingConfig);
+            if (($arrayKeys[0] ?? null) === 0) {
+                // We're dealing with a list of items (numeric array); will be handled by the method that fetches added items
+                // Here, we'll just get any extra items the user added
+
+                $outgoing = $this->getOutgoingConfigItem($rootKey);
+                $incoming = $this->getIncomingConfigItem($rootKey);
+
+                foreach ($outgoing->items as $i => $outgoingItem) {
+                    if ($outgoingItem->value instanceof Node\Scalar
+                        && $incoming->items[0]->value instanceof Node\Scalar) {
+                        $inIncoming = Arr::first(
+                            $incoming->items,
+                            fn(Node\Expr\ArrayItem $incomingItem) => $incomingItem->value->value === $outgoingItem->value->value
+                        );
+                        if (!$inIncoming) {
+                            $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
+                        }
+                    } else if ($outgoingItem->value instanceof Node\Expr\ClassConstFetch
+                        && $incoming->items[0]->value instanceof Node\Expr\ClassConstFetch) {
+                        // Handle ::class statements
+                        $inIncoming = Arr::first(
+                            $incoming->items,
+                            function (Node\Expr\ArrayItem $incomingItem) use ($outgoingItem) {
+                                // Rough equality check using final segments of class name
+                                $classNamePartsReversed = array_reverse($outgoingItem->value->class->parts);
+                                foreach ($classNamePartsReversed as $i => $classNamePart) {
+                                    $incomingClassNamePartsReversed = array_reverse($incomingItem->value->class->parts);
+                                    if (isset($incomingClassNamePartsReversed[$i])
+                                    && $incomingClassNamePartsReversed[$i] === $classNamePart) {
+                                        return true;
+                                    }
+                                }
+                            }
+                        );
+                        if (!$inIncoming) {
+                            $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
+                        }
+                    } else {
+                        $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
+                    }
+                }
+                return;
+            }
+        }
+
+        foreach ($userCurrentConfig as $key => $value) {
+            $fullKey = $this->getFullKey($key, $rootKey);
+
+            $outgoing = $this->getOutgoingConfigItem($fullKey);
+
+            if ($this->wasKeyMoved($fullKey)) {
+                $this->userFacingChanges[] = [
+                    'type' => self::CHANGE_RENAMED,
+                    'key' => $fullKey,
+                    'new_key' => $this->movedKeys[$fullKey],
+                    'new_value' => $outgoing,
+                    'description' => "- `$fullKey` will be moved to `{$this->movedKeys[$fullKey]}`.",
+                ];
+                continue;
+            }
+
+            if (!array_key_exists($key, $incomingConfig)) {
+                $this->userFacingChanges[] = [
+                    'type' => self::CHANGE_REMOVED,
+                    'key' => $fullKey,
+                    'description' => "- `$fullKey` will be removed.",
+                ];
+                continue;
+            }
+
+            if ($this->canModifyKey($fullKey) && is_array($value)) {
+                // Recurse into the array
+                $this->fetchRemovedOrRenamedItems($value, data_get($incomingConfig, $key), $fullKey);
+            } else {
+                // This key is present in both existing and incoming configs
+                // Save the user's value so we can replace the default in the incoming
+                $this->catchUps[$fullKey] = $outgoing;
+            }
+
+        }
+    }
+
+    /**
+     * Report the new items in the incoming config
+     */
+    protected function fetchAddedItems(array $userCurrentConfig, array $incomingConfig, string $rootKey = '')
+    {
+        if (is_array($incomingConfig)) {
+            $arrayKeys = array_keys($incomingConfig);
+            if (($arrayKeys[0] ?? null) === 0) {
+                // We're dealing with a list of items (numeric array)
+                $diff = array_diff($incomingConfig, $userCurrentConfig);
+                if (!empty($diff)) {
+                    foreach ($diff as $item) {
+                        $this->userFacingChanges[] = [
+                            'type' => self::CHANGE_ARRAY_ITEM_ADDED,
+                            'key' => $rootKey,
+                            'value' => $item,
+                            'description' => "- '$item' will be added to `$rootKey`.",
+                        ];
+                    }
+                }
+                return;
+            }
+        }
+
+        foreach ($incomingConfig as $key => $value) {
+            $fullKey = $this->getFullKey($key, $rootKey);
+
+            if (!$this->canModifyKey($fullKey)) {
+                continue;
+            }
+
+            if (Arr::exists($userCurrentConfig, $key)) {
+                if (is_array($value)) {
+                    // Recurse into array
+                    $this->fetchAddedItems(data_get($userCurrentConfig, $key), $value, $fullKey);
+                }
+            } else {
+                $this->userFacingChanges[] = [
+                    'type' => self::CHANGE_ADDED,
+                    'key' => $fullKey,
+                    'description' => "- `{$fullKey}` will be added.",
+                ];
+            }
+
+        }
+
+    }
+
+    protected function applyChanges()
+    {
+        // First, get the new config file and replace defaults with user's old values
+        $ast = $this->getIncomingConfigFileAst();
+        foreach ($this->catchUps as $key => $value) {
+            if (preg_match('/.*\.\d+$/', $key)) {
+                // Array item
+                $this->pushValue($ast, preg_replace('/.\d+$/', '', $key), $value);
+            } else {
+                $this->setValue($ast, $key, $value);
+            }
+        }
+
+
+        // Next, make the "migration" changes (rename config keys)
+        foreach ($this->userFacingChanges as $change) {
+            switch ($change['type']) {
+                case self::CHANGE_REMOVED:
+                    // Do nothing; the new config already doesn't have this
+                    break;
+                case self::CHANGE_RENAMED:
+                    $this->setValue($ast, $change['new_key'], $change['new_value']);
+                    break;
+                case self::CHANGE_ARRAY_ITEM_ADDED:
+                    $this->pushValue($ast, $change['key'], $change['value']);
+                    break;
+            }
+        }
+
+        // Finally, print out the changes into the user's config file (saving the old one as a backup)
+        $prettyPrinter = new PrettyPrinter\Standard(['shortArraySyntax' => true]);
+        $newCode = $prettyPrinter->printFormatPreserving($ast, $this->incomingConfigFileOriginalAst, $this->incomingConfigFileOriginalTokens);
+
+        $outputFile = $this->configFiles['user_relative'];
+        rename($outputFile, "$outputFile.bak");
+        copy($this->configFiles['package_absolute'], $outputFile);
+        file_put_contents($outputFile, $newCode);
+    }
+
+    protected function getOutgoingConfigItem(string $fullKey): ?Node\Expr
+    {
+        $ast = $this->getOutgoingConfigAst();
+        return $this->getAstItem($ast, $fullKey);
+    }
+
+    protected function getIncomingConfigItem(string $fullKey): ?Node\Expr
+    {
+        $ast = $this->getIncomingConfigFileAst();
+        return $this->getAstItem($ast, $fullKey);
+    }
+
+    protected function getAstItem(array $ast, string $fullKey): ?Node\Expr
+    {
+        $keySegments = explode('.', $fullKey);
+        $nodeFinder = new NodeFinder;
+        /** @var Node\Expr\ArrayItem[] $configArray */
+        $configArray = $nodeFinder->findFirst(
+            $ast, fn(Node $node) => $node instanceof Node\Stmt\Return_
+        )->expr->items;
+
+        $searchArray = $configArray;
+        try {
+            while (count($keySegments)) {
+                $nextKeySegment = array_shift($keySegments);
+                foreach ($searchArray as $item) {
+                    if ($item->key->value === $nextKeySegment) {
+                        break;
+                    }
+                }
+                if (count($keySegments)) {
+                    $searchArray = $item->value->items ?? [];
+                } else {
+                    return $item->value;
+                }
+            }
+        } catch (\Throwable $e) {
+            return null;
+        }
+    }
+
+    protected function getOutgoingConfigAst(): ?array
+    {
+        if (!empty($this->outgoingConfigFileAst)) {
+            return $this->outgoingConfigFileAst;
+        }
+
+        $sourceCode = file_get_contents($this->configFiles['user_relative']);
+
+        $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
+        return $this->outgoingConfigFileAst = $parser->parse($sourceCode);
+    }
+
+    protected function getIncomingConfigFileAst(): ?array
+    {
+        if (!empty($this->incomingConfigFileAstForModification)) {
+            return $this->incomingConfigFileAstForModification;
+        }
+
+        $sourceCode = file_get_contents($this->configFiles['package_absolute']);
+
+        // Doing this because we need to preserve the formatting when printing later
+        $lexer = new Lexer\Emulative([
+            'usedAttributes' => [
+                'comments',
+                'startLine', 'endLine',
+                'startTokenPos', 'endTokenPos',
+            ],
+        ]);
+        $parser = new Parser\Php7($lexer);
+        $this->incomingConfigFileOriginalAst = $parser->parse($sourceCode);
+        $this->incomingConfigFileOriginalTokens = $lexer->getTokens();
+        $traverser = new NodeTraverser();
+        $traverser->addVisitor(new NodeVisitor\CloningVisitor());
+        $clonedAst = $traverser->traverse($this->incomingConfigFileOriginalAst);
+
+        return $this->incomingConfigFileAstForModification = $clonedAst;
+    }
+
+    protected function setValue($ast, string $key, $newValue)
+    {
+        $keySegments = explode('.', $key);
+        $nodeFinder = new NodeFinder;
+        /** @var Node\Expr\ArrayItem[] $configArray */
+        $configArray = $nodeFinder->findFirst(
+            $ast, fn(Node $node) => $node instanceof Node\Stmt\Return_
+        )->expr->items;
+
+        $searchArray = $configArray;
+        while (count($keySegments)) {
+            $nextKeySegment = array_shift($keySegments);
+            foreach ($searchArray as $item) {
+                if ($item->key->value === $nextKeySegment) {
+                    break;
+                }
+            }
+
+            if (count($keySegments)) {
+                $searchArray = $item->value->items ?? [];
+            } else {
+                $item->value = $newValue;
+                return;
+            }
+        }
+    }
+
+    protected function pushValue($ast, string $arrayKey, $newValue)
+    {
+        $keySegments = explode('.', $arrayKey);
+        $nodeFinder = new NodeFinder;
+        /** @var Node\Expr\ArrayItem[] $configArray */
+        $configArray = $nodeFinder->findFirst(
+            $ast, fn(Node $node) => $node instanceof Node\Stmt\Return_
+        )->expr->items;
+
+        $searchArray = $configArray;
+        while (count($keySegments)) {
+            $nextKeySegment = array_shift($keySegments);
+            foreach ($searchArray as $item) {
+                if ($item->key->value === $nextKeySegment) {
+                    break;
+                }
+            }
+            if (count($keySegments)) {
+                $searchArray = $item->value->items ?? [];
+            } else {
+                $item->value->items[] = $newValue;
+                return;
+            }
+        }
+    }
+
+    /**
+     * Resolve config item key with dot notation
+     */
+    private function getFullKey(string $key, string $rootKey = ''): string
+    {
+        if (empty($rootKey)) {
+            return $key;
+        }
+
+        return "$rootKey.$key";
+    }
+
+    public function dontTouch(string ...$keys): self
+    {
+        $this->dontTouchKeys += $keys;
+        return $this;
+    }
+
+    protected function canModifyKey(string $key): bool
+    {
+        return !in_array($key, $this->dontTouchKeys);
+    }
+
+    public function move(string $oldKey, string $newKey): self
+    {
+        $this->movedKeys[$oldKey] = $newKey;
+        return $this;
+    }
+
+    protected function wasKeyMoved(string $oldKey): bool
+    {
+        return array_key_exists($oldKey, $this->movedKeys);
+    }
+
+}