Something went wrong. Try again.
Parse and validate AT Protocol Lexicons with DTO generation for Laravel
Something went wrong. Try again.
11 kB · 352 lines
PHP
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353<?php
namespace SocialDept\AtpSchema\Generator;
use SocialDept\AtpSchema\Contracts\DataGenerator;use SocialDept\AtpSchema\Data\LexiconDocument;use SocialDept\AtpSchema\Parser\SchemaLoader;use SocialDept\AtpSchema\Parser\TypeParser;
class DTOGenerator implements DataGenerator{ /** * Schema loader for loading lexicon documents. */ protected SchemaLoader $schemaLoader;
/** * Type parser for parsing type definitions. */ protected TypeParser $typeParser;
/** * Namespace resolver for converting NSIDs to PHP namespaces. */ protected NamespaceResolver $namespaceResolver;
/** * Template renderer for generating PHP code. */ protected TemplateRenderer $templateRenderer;
/** * File writer for writing generated files. */ protected FileWriter $fileWriter;
/** * Class generator for generating PHP classes. */ protected ClassGenerator $classGenerator;
/** * Base namespace for generated classes. */ protected string $baseNamespace;
/** * Output directory for generated files. */ protected string $outputDirectory;
/** * Create a new DTOGenerator. */ public function __construct( SchemaLoader $schemaLoader, string $baseNamespace = 'App\\Lexicons', string $outputDirectory = 'app/Lexicons', ?TypeParser $typeParser = null, ?NamespaceResolver $namespaceResolver = null, ?TemplateRenderer $templateRenderer = null, ?FileWriter $fileWriter = null, ?ClassGenerator $classGenerator = null ) { $this->schemaLoader = $schemaLoader; $this->baseNamespace = rtrim($baseNamespace, '\\'); $this->outputDirectory = rtrim($outputDirectory, '/'); $this->typeParser = $typeParser ?? new TypeParser(schemaLoader: $schemaLoader); $this->namespaceResolver = $namespaceResolver ?? new NamespaceResolver($baseNamespace); $this->templateRenderer = $templateRenderer ?? new TemplateRenderer(); $this->fileWriter = $fileWriter ?? new FileWriter();
// Initialize ClassGenerator with proper naming converter $naming = new NamingConverter($this->baseNamespace); $this->classGenerator = $classGenerator ?? new ClassGenerator($naming); }
/** * Generate PHP class files from Lexicon definition. */ public function generate(LexiconDocument $schema): string { return $this->classGenerator->generate($schema); }
/** * Generate and write class file to disk. */ public function generateAndSave(LexiconDocument $schema, string $outputPath): string { $code = $this->generate($schema); $this->fileWriter->write($outputPath, $code);
return $outputPath; }
/** * Generate class content without writing to disk. */ public function preview(LexiconDocument $schema): string { return $this->generate($schema); }
/** * Set the base namespace for generated classes. */ public function setBaseNamespace(string $namespace): void { $this->baseNamespace = rtrim($namespace, '\\'); $this->namespaceResolver = new NamespaceResolver($this->baseNamespace); }
/** * Set the output path for generated classes. */ public function setOutputPath(string $path): void { $this->outputDirectory = rtrim($path, '/'); }
/** * Generate DTO classes from NSID. */ public function generateByNsid(string $nsid, array $options = []): array { $document = $this->schemaLoader->load($nsid);
return $this->generateFromDocument($document, $options); }
/** * Generate DTO classes from a lexicon document. */ public function generateFromDocument(LexiconDocument $document, array $options = []): array { $generatedFiles = [];
// Generate main class if it's a record or object $mainDef = $document->getMainDefinition(); $mainType = $mainDef['type'] ?? null;
if ($document->isRecord()) { $file = $this->generateRecordClass($document, $options); $generatedFiles[] = $file; } elseif ($mainType === 'object') { // Generate for standalone object types (like strongRef) $file = $this->generateRecordClass($document, $options); $generatedFiles[] = $file; }
// Generate classes for other definitions foreach ($document->getDefinitionNames() as $defName) { if ($defName === 'main') { continue; }
$definition = $document->getDefinition($defName);
if (isset($definition['type']) && $definition['type'] === 'object') { $file = $this->generateDefinitionClass($document, $defName, $options); $generatedFiles[] = $file; } }
return $generatedFiles; }
/** * Generate code for a record (without writing to disk). */ protected function generateRecordCode(LexiconDocument $document): string { $namespace = $this->namespaceResolver->resolveNamespace($document->getNsid()); $className = $this->namespaceResolver->resolveClassName($document->getNsid());
$mainDef = $document->getMainDefinition(); $recordSchema = $mainDef['record'] ?? [];
$properties = $this->extractProperties($recordSchema, $document);
return $this->templateRenderer->render('record', [ 'namespace' => $namespace, 'className' => $className, 'nsid' => $document->getNsid(), 'description' => $document->description, 'properties' => $properties, ]); }
/** * Generate a record class from a lexicon document. */ protected function generateRecordClass(LexiconDocument $document, array $options = []): string { // Use ClassGenerator for proper code generation $code = $this->classGenerator->generate($document);
$naming = $this->classGenerator->getNaming(); $namespace = $naming->nsidToNamespace($document->getNsid()); $className = $naming->toClassName($document->id->getName()); $filePath = $this->getFilePath($namespace, $className);
if (! ($options['dryRun'] ?? false)) { $this->fileWriter->write($filePath, $code); }
return $filePath; }
/** * Generate a class for a specific definition. */ protected function generateDefinitionClass(LexiconDocument $document, string $defName, array $options = []): string { // Create a temporary document for this specific definition $definition = $document->getDefinition($defName);
// Build a temporary lexicon document for the object definition $objectNsid = $document->getNsid().'.'.$defName; $tempSchema = [ 'id' => $objectNsid, 'lexicon' => 1, 'description' => $definition['description'] ?? null, 'defs' => [ 'main' => [ 'type' => 'object', 'properties' => $definition['properties'] ?? [], 'required' => $definition['required'] ?? [], 'description' => $definition['description'] ?? null, ], ], ];
$tempDocument = \SocialDept\AtpSchema\Data\LexiconDocument::fromArray($tempSchema);
// Use ClassGenerator for proper code generation $code = $this->classGenerator->generate($tempDocument);
$naming = $this->classGenerator->getNaming(); $namespace = $naming->nsidToNamespace($objectNsid); $className = $naming->toClassName($defName); $filePath = $this->getFilePath($namespace, $className);
if (! ($options['dryRun'] ?? false)) { $this->fileWriter->write($filePath, $code); }
return $filePath; }
/** * Extract properties from a schema definition. * * @return array<array{name: string, type: string, phpType: string, required: bool, description: ?string}> */ protected function extractProperties(array $schema, LexiconDocument $document): array { $properties = []; $schemaProperties = $schema['properties'] ?? []; $required = $schema['required'] ?? [];
foreach ($schemaProperties as $name => $propSchema) { $properties[] = [ 'name' => $name, 'type' => $propSchema['type'] ?? 'unknown', 'phpType' => $this->mapToPhpType($propSchema), 'required' => in_array($name, $required), 'description' => $propSchema['description'] ?? null, ]; }
return $properties; }
/** * Map lexicon type to PHP type. */ protected function mapToPhpType(array $typeSchema): string { $type = $typeSchema['type'] ?? 'unknown';
return match ($type) { 'null' => 'null', 'boolean' => 'bool', 'integer' => 'int', 'string' => 'string', 'bytes' => 'string', 'array' => 'array', 'object' => 'array', 'unknown' => 'mixed', default => 'mixed', }; }
/** * Get the file path for a generated class. */ protected function getFilePath(string $namespace, string $className): string { // Remove base namespace from full namespace $relativePath = str_replace($this->baseNamespace.'\\', '', $namespace); $relativePath = str_replace('\\', '/', $relativePath);
return $this->outputDirectory.'/'.$relativePath.'/'.$className.'.php'; }
/** * Validate generated code. */ public function validate(string $code): bool { // Basic syntax check using token_get_all $tokens = @token_get_all($code);
return $tokens !== false; }
/** * Get generated file metadata. */ public function getMetadata(string $nsid): array { $document = $this->schemaLoader->load($nsid);
$namespace = $this->namespaceResolver->resolveNamespace($document->getNsid()); $className = $this->namespaceResolver->resolveClassName($document->getNsid());
return [ 'nsid' => $nsid, 'namespace' => $namespace, 'className' => $className, 'fullyQualifiedName' => $namespace.'\\'.$className, 'type' => $document->isRecord() ? 'record' : 'object', ]; }
/** * Set output options. */ public function setOptions(array $options): void { if (isset($options['baseNamespace'])) { $this->baseNamespace = rtrim($options['baseNamespace'], '\\'); $this->namespaceResolver = new NamespaceResolver($this->baseNamespace); }
if (isset($options['outputDirectory'])) { $this->outputDirectory = rtrim($options['outputDirectory'], '/'); } }}