Skip to content

Commit fac37f6

Browse files
committed
Initial setup for file serve
With this change we provide an internal webserver in the application. And introduce a watch on changes. This introduces a new way of working with guides, allowing users to edit documentation without caring about rerendering. Optimizations are possible like parsing and rendering a single file to make rerendering faster.
1 parent 6a2510e commit fac37f6

File tree

15 files changed

+1512
-478
lines changed

15 files changed

+1512
-478
lines changed

app/Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM php:8.4-cli
2+
3+
COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie
4+
5+
RUN apt update && apt install -y git
6+
7+
RUN docker-php-ext-configure pcntl --enable-pcntl \
8+
&& docker-php-ext-install pcntl \
9+
&& pie install arnaud-lb/inotify
10+

composer.lock

Lines changed: 896 additions & 275 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/filesystem/src/FileSystem.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,6 @@ public function listContents(string $directory = '', bool $recursive = false): a
5454

5555
/** @return StorageAttributes[] */
5656
public function find(SpecificationInterface $specification): iterable;
57+
58+
public function isDirectory(string $path): bool;
5759
}

packages/filesystem/src/FlySystemAdapter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,9 @@ public function find(SpecificationInterface $specification): iterable
9292
{
9393
return $this->filesystem->find($specification);
9494
}
95+
96+
public function isDirectory(string $path): bool
97+
{
98+
return $this->filesystem->isDirectory($path);
99+
}
95100
}

packages/filesystem/src/FlysystemV1/FlysystemV1.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,9 @@ public function find(SpecificationInterface $specification): iterable
7575
yield new \phpDocumentor\FileSystem\FlysystemV1\StorageAttributes($file);
7676
}
7777
}
78+
79+
public function isDirectory(string $path): bool
80+
{
81+
return $this->filesystem->directoryExists($path);
82+
}
7883
}

packages/filesystem/src/FlysystemV3/FlysystemV3.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public function has(string $path): bool
3535
return $this->filesystem->has($path);
3636
}
3737

38+
public function isDirectory(string $path): bool
39+
{
40+
return $this->filesystem->directoryExists($path);
41+
}
42+
3843
public function readStream(string $path): mixed
3944
{
4045
return $this->filesystem->readStream($path);

packages/guides-cli/composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
"symfony/config": "^5.4 || ^6.3 || ^7.0",
3333
"symfony/console": "^5.4 || ^6.3 || ^7.0",
3434
"symfony/dependency-injection": "^5.4 || ^6.3 || ^7.0",
35-
"symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0"
35+
"symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0",
36+
"react/http": "^v1.11",
37+
"react/socket": "^v1.16",
38+
"league/mime-type-detection": "^1.16"
3639
},
3740
"bin": [
3841
"bin/guides"

packages/guides-cli/resources/config/services.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
use phpDocumentor\Guides\Cli\Application;
77
use phpDocumentor\Guides\Cli\Command\ProgressBarSubscriber;
88
use phpDocumentor\Guides\Cli\Command\Run;
9+
use phpDocumentor\Guides\Cli\Command\Serve;
910
use phpDocumentor\Guides\Cli\Command\WorkingDirectorySwitcher;
11+
use phpDocumentor\Guides\Cli\Command\SettingsBuilder;
12+
use phpDocumentor\Guides\Cli\Internal\RunCommand;
13+
use phpDocumentor\Guides\Cli\Internal\RunCommandHandler;
1014
use Psr\Clock\ClockInterface;
1115
use Psr\EventDispatcher\EventDispatcherInterface;
1216
use Psr\Log\LoggerInterface;
@@ -26,6 +30,10 @@
2630
->public()
2731
->tag('phpdoc.guides.cli.command')
2832

33+
->set(Serve::class)
34+
->public()
35+
->tag('phpdoc.guides.cli.command')
36+
2937
->set(NativeClock::class)
3038
->alias(ClockInterface::class, NativeClock::class)
3139

@@ -44,5 +52,9 @@
4452
->set(WorkingDirectorySwitcher::class)
4553
->tag('event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => '__invoke'])
4654

47-
->set(ProgressBarSubscriber::class);
55+
->set(ProgressBarSubscriber::class)
56+
->set(SettingsBuilder::class)
57+
->set(RunCommandHandler::class)
58+
->tag('phpdoc.guides.command', ['command' => RunCommand::class])
59+
;
4860
};

packages/guides-cli/src/Command/Run.php

Lines changed: 11 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Monolog\Logger;
2626
use phpDocumentor\FileSystem\Finder\Exclude;
2727
use phpDocumentor\FileSystem\FlySystemAdapter;
28+
use phpDocumentor\Guides\Cli\Internal\RunCommand;
2829
use phpDocumentor\Guides\Cli\Logger\SpyProcessor;
2930
use phpDocumentor\Guides\Compiler\CompilerContext;
3031
use phpDocumentor\Guides\Event\PostProjectNodeCreated;
@@ -71,47 +72,13 @@ public function __construct(
7172
private readonly ClockInterface $clock,
7273
private readonly EventDispatcher $eventDispatcher,
7374
private readonly ProgressBarSubscriber $progressBarSubscriber,
75+
private SettingsBuilder $settingsBuilder,
7476
) {
7577
parent::__construct('run');
7678

77-
$this->addArgument(
78-
'input',
79-
InputArgument::OPTIONAL,
80-
'Directory which holds the files to render',
81-
);
82-
$this->addOption(
83-
'output',
84-
null,
85-
InputOption::VALUE_REQUIRED,
86-
'Directory to write rendered files to',
87-
);
88-
89-
$this->addOption(
90-
'input-file',
91-
null,
92-
InputOption::VALUE_REQUIRED,
93-
'If set, only the specified file is parsed, relative to the directory specified in "input"',
94-
);
95-
96-
$this->addOption(
97-
'exclude-path',
98-
null,
99-
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
100-
'Paths to exclude, doc files in these directories will not be parsed',
101-
);
79+
$this->settingsBuilder ??= new SettingsBuilder($this->eventDispatcher, $this->settingsManager, $this->clock);
80+
$this->settingsBuilder->configureCommand($this);
10281

103-
$this->addOption(
104-
'input-format',
105-
null,
106-
InputOption::VALUE_REQUIRED,
107-
'Format of the input can be "RST", or "Markdown"',
108-
);
109-
$this->addOption(
110-
'output-format',
111-
null,
112-
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
113-
'Format of the input can be "html" and/or "interlink"',
114-
);
11582
$this->addOption(
11683
'log-path',
11784
null,
@@ -132,13 +99,6 @@ public function __construct(
13299
'If set, returns a non-zero exit code as soon as any errors occur',
133100
);
134101

135-
$this->addOption(
136-
'theme',
137-
null,
138-
InputOption::VALUE_REQUIRED,
139-
'The theme used for rendering',
140-
);
141-
142102
$this->addOption(
143103
'progress',
144104
null,
@@ -159,93 +119,11 @@ public function registerProgressBar(ConsoleOutputInterface $output): void
159119
$this->progressBarSubscriber->subscribe($output, $this->eventDispatcher);
160120
}
161121

162-
private function getSettingsOverriddenWithInput(InputInterface $input): ProjectSettings
163-
{
164-
$settings = $this->settingsManager->getProjectSettings();
165-
166-
if ($settings->isShowProgressBar()) {
167-
$settings->setShowProgressBar($input->getOption('progress'));
168-
}
169-
170-
if ($input->getArgument('input')) {
171-
$settings->setInput((string) $input->getArgument('input'));
172-
}
173-
174-
if ($input->getOption('output')) {
175-
$settings->setOutput((string) $input->getOption('output'));
176-
}
177-
178-
if ($input->getOption('input-file')) {
179-
$inputFile = (string) $input->getOption('input-file');
180-
$pathInfo = pathinfo($inputFile);
181-
$settings->setInputFile($pathInfo['filename']);
182-
if (!empty($pathInfo['extension'])) {
183-
$settings->setInputFormat($pathInfo['extension']);
184-
}
185-
}
186-
187-
if ($input->getOption('input-format')) {
188-
$settings->setInputFormat((string) $input->getOption('input-format'));
189-
}
190-
191-
if ($input->getOption('log-path')) {
192-
$settings->setLogPath((string) $input->getOption('log-path'));
193-
}
194-
195-
if ($input->getOption('fail-on-error')) {
196-
$settings->setFailOnError(LogLevel::ERROR);
197-
}
198-
199-
if ($input->getOption('fail-on-log')) {
200-
$settings->setFailOnError(LogLevel::WARNING);
201-
}
202-
203-
if (count($input->getOption('output-format')) > 0) {
204-
$settings->setOutputFormats($input->getOption('output-format'));
205-
}
206-
207-
if ($input->getOption('theme')) {
208-
$settings->setTheme((string) $input->getOption('theme'));
209-
}
210-
211-
if (method_exists($settings, 'setExcludes')) {
212-
/** @var list<string> $excludePaths */
213-
$excludePaths = (array) $input->getOption('exclude-path');
214-
if ($excludePaths !== []) {
215-
$settings->setExcludes(
216-
$settings->getExcludes()->withPaths($excludePaths),
217-
);
218-
}
219-
}
220-
221-
return $settings;
222-
}
223-
224122
protected function execute(InputInterface $input, OutputInterface $output): int
225123
{
226-
$settings = $this->getSettingsOverriddenWithInput($input);
227-
$inputDir = $settings->getInput();
228-
if (!is_dir($inputDir)) {
229-
throw new RuntimeException(sprintf('Input directory "%s" was not found! ' . "\n" .
230-
'Run "vendor/bin/guides -h" for information on how to configure this command.', $inputDir));
231-
}
232-
233-
$projectNode = new ProjectNode(
234-
$settings->getTitle() === '' ? null : $settings->getTitle(),
235-
$settings->getVersion() === '' ? null : $settings->getVersion(),
236-
$settings->getRelease() === '' ? null : $settings->getRelease(),
237-
$settings->getCopyright() === '' ? null : $settings->getCopyright(),
238-
$this->clock->now(),
239-
);
240-
241-
$event = new PostProjectNodeCreated($projectNode, $settings);
242-
$event = $this->eventDispatcher->dispatch($event);
243-
assert($event instanceof PostProjectNodeCreated);
244-
$projectNode = $event->getProjectNode();
245-
$settings = $event->getSettings();
246-
247-
$outputDir = $settings->getOutput();
248-
$sourceFileSystem = FlySystemAdapter::createForPath($settings->getInput());
124+
$this->settingsBuilder->overrideWithInput($input);
125+
$projectNode = $this->settingsBuilder->createProjectNode();
126+
$settings = $this->settingsBuilder->getSettings();
249127

250128
$logPath = $settings->getLogPath();
251129
if ($logPath === 'php://stder') {
@@ -260,57 +138,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
260138
$this->logger->pushProcessor($spyProcessor);
261139
}
262140

263-
$documents = [];
264-
265-
266141
if ($output instanceof ConsoleOutputInterface && $settings->isShowProgressBar()) {
267142
$this->progressBarSubscriber->subscribe($output, $this->eventDispatcher);
268143
}
269144

270-
if ($settings->getInputFile() === '') {
271-
$documents = $this->commandBus->handle(
272-
new ParseDirectoryCommand(
273-
$sourceFileSystem,
274-
'',
275-
$settings->getInputFormat(),
276-
$projectNode,
277-
$this->getExclude($settings, $input),
278-
),
279-
);
280-
} else {
281-
$documents[] = $this->commandBus->handle(
282-
new ParseFileCommand(
283-
$sourceFileSystem,
284-
'',
285-
$settings->getInputFile(),
286-
$settings->getInputFormat(),
287-
1,
288-
$projectNode,
289-
true,
290-
),
291-
);
292-
}
293-
294-
$this->themeManager->useTheme($settings->getTheme());
295-
296-
$documents = $this->commandBus->handle(new CompileDocumentsCommand($documents, new CompilerContext($projectNode)));
297-
298-
$destinationFileSystem = FlySystemAdapter::createForPath($outputDir);
145+
$documents = $this->commandBus->handle(
146+
new RunCommand($settings, $projectNode, $input),
147+
);
299148

300149
$outputFormats = $settings->getOutputFormats();
301-
302-
foreach ($outputFormats as $format) {
303-
$this->commandBus->handle(
304-
new RenderCommand(
305-
$format,
306-
$documents,
307-
$sourceFileSystem,
308-
$destinationFileSystem,
309-
$projectNode,
310-
),
311-
);
312-
}
313-
150+
$outputDir = $settings->getOutput();
314151
if ($output->isQuiet() === false) {
315152
$lastFormat = '';
316153

@@ -331,31 +168,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int
331168

332169
return Command::SUCCESS;
333170
}
334-
335-
private function getExclude(ProjectSettings $settings, InputInterface|null $input = null): Exclude|SpecificationInterface|null
336-
{
337-
if (method_exists($settings, 'getExcludes')) {
338-
return $settings->getExcludes();
339-
}
340-
341-
if ($input === null) {
342-
return null;
343-
}
344-
345-
if ($input->getOption('exclude-path')) {
346-
/** @var string[] $excludedPaths */
347-
$excludedPaths = (array) $input->getOption('exclude-path');
348-
$excludedSpecifications = array_map(static fn (string $path) => new NotSpecification(new InPath(new Path($path))), $excludedPaths);
349-
$excludedSpecification = array_shift($excludedSpecifications);
350-
assert($excludedSpecification !== null);
351-
352-
return array_reduce(
353-
$excludedSpecifications,
354-
static fn (SpecificationInterface $carry, SpecificationInterface $spec) => new OrSpecification($carry, $spec),
355-
$excludedSpecification,
356-
);
357-
}
358-
359-
return null;
360-
}
361171
}

0 commit comments

Comments
 (0)