FileSourceFetcher.php•6.76 kB
<?php
declare(strict_types=1);
namespace Butschster\ContextGenerator\Source\File;
use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Butschster\ContextGenerator\Lib\Content\ContentBuilderFactory;
use Butschster\ContextGenerator\Lib\Finder\FinderInterface;
use Butschster\ContextGenerator\Modifier\ModifiersApplierInterface;
use Butschster\ContextGenerator\Source\Fetcher\SourceFetcherInterface;
use Butschster\ContextGenerator\Source\SourceInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\SplFileInfo;
/**
 * Enhanced fetcher for file sources with extended Symfony Finder features
 * @implements SourceFetcherInterface<FileSource>
 */
final readonly class FileSourceFetcher implements SourceFetcherInterface
{
    public function __construct(
        private string $basePath,
        private FinderInterface $finder,
        private ContentBuilderFactory $builderFactory = new ContentBuilderFactory(),
        #[LoggerPrefix(prefix: 'file-source')]
        private ?LoggerInterface $logger = null,
    ) {}
    public function supports(SourceInterface $source): bool
    {
        $isSupported = $source instanceof FileSource;
        $this->logger?->debug('Checking if source is supported', [
            'sourceType' => $source::class,
            'isSupported' => $isSupported,
        ]);
        return $isSupported;
    }
    public function fetch(SourceInterface $source, ModifiersApplierInterface $modifiersApplier): string
    {
        if (!$source instanceof FileSource) {
            $errorMessage = 'Source must be an instance of FileSource';
            $this->logger?->error($errorMessage, [
                'sourceType' => $source::class,
            ]);
            throw new \InvalidArgumentException($errorMessage);
        }
        $this->logger?->info('Fetching file source content', [
            'description' => $source->getDescription(),
            'basePath' => $this->basePath,
            'hasModifiers' => !empty($source->modifiers),
        ]);
        $this->logger?->debug('Creating content builder');
        $builder = $this->builderFactory
            ->create()
            ->addTitle($source->getDescription());
        // Execute find operation and get the result
        $this->logger?->debug('Finding files', [
            'in' => $source->in(),
            'files' => $source->files(),
        ]);
        try {
            $finderResult = $this->finder->find($source, $this->basePath, $source->treeView->getOptions());
            $fileCount = $finderResult->count();
            $this->logger?->debug('Files found', ['fileCount' => $fileCount]);
        } catch (\Throwable $e) {
            if (\str_contains($e->getMessage(), 'must call one of in() or append() methods')) {
                $errorMessage = \sprintf(
                    'Some directories or files contain invalid paths: %s',
                    \implode(
                        ', ',
                        [
                            ...(array) $source->in(),
                            ...(array) $source->files(),
                        ],
                    ),
                );
                $this->logger?->error($errorMessage, [
                    'error' => $e->getMessage(),
                ]);
                throw new \RuntimeException($errorMessage);
            }
            $errorMessage = \sprintf('Error while finding files: %s', $e->getMessage());
            $this->logger?->error($errorMessage, [
                'error' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
            ]);
            throw new \RuntimeException($errorMessage);
        }
        // Generate tree view if requested
        if ($source->treeView->enabled) {
            $this->logger?->debug('Adding tree view to output');
            $builder->addTreeView($finderResult->treeView);
        }
        // Process each file
        $this->logger?->debug('Processing files');
        foreach ($finderResult->files as $index => $file) {
            if (!$file instanceof SplFileInfo && $file instanceof \SplFileInfo) {
                $this->logger?->debug('Converting SplFileInfo to Symfony SplFileInfo', [
                    'pathname' => $file->getPathname(),
                ]);
                $file = new SplFileInfo(
                    file: $file->getPathname(),
                    relativePath: $file->getPath(),
                    relativePathname: $file->getPathname(),
                );
            }
            $relativePath = \trim(\str_replace($this->basePath, '', $file->getPath()));
            $fileName = $file->getFilename();
            $filePath = empty($relativePath) ? $fileName : "$relativePath/$fileName";
            $this->logger?->debug('Processing file', [
                'file' => $filePath,
                'index' => $index + 1,
                'total' => $fileCount,
                'size' => $file->getSize(),
            ]);
            $language = $this->detectLanguage($filePath);
            $content = $modifiersApplier->apply($this->getContent($file, $source), $fileName);
            $this->logger?->debug('Adding file to content', [
                'file' => $filePath,
                'language' => $language,
                'contentLength' => \strlen($content),
            ]);
            $builder->addCodeBlock(
                code: $content,
                language: $language,
                path: $filePath,
            );
        }
        $content = $builder->build();
        $this->logger?->info('File source content fetched successfully', [
            'fileCount' => $fileCount,
            'contentLength' => \strlen($content),
        ]);
        // Return built content
        return $content;
    }
    /**
     * Get and optionally modify the content of a file
     *
     * @param SplFileInfo $file The file info object
     * @param FileSource $source The source containing modifiers
     * @return string The file content, possibly modified
     */
    protected function getContent(SplFileInfo $file, SourceInterface $source): string
    {
        \assert($source instanceof FileSource);
        $filePath = $file->getRelativePathname();
        $this->logger?->debug('Reading file content', ['file' => $filePath]);
        return $file->getContents();
    }
    private function detectLanguage(string $filePath): ?string
    {
        $extension = \pathinfo($filePath, PATHINFO_EXTENSION);
        $this->logger?->debug('Detecting language for file', [
            'file' => $filePath,
            'extension' => $extension,
        ]);
        if (empty($extension)) {
            return null;
        }
        return $extension;
    }
}