small / class-manipulator
Modern PHP source-code manipulation for classes, interfaces, traits and enums.
Package info
git.small-project.dev/lib/small-class-manipulator.git
pkg:composer/small/class-manipulator
Requires
- php: >=8.3
- ext-mbstring: *
- nikic/php-parser: ^5.6
Requires (Dev)
- pestphp/pest: ^2.36
- phpstan/phpstan: ^2.1
README
Small Class Manipulator

Small Class Manipulator reads, creates and rewrites PHP classes, interfaces, traits and enums through an AST-backed API built on nikic/php-parser.
Requirements
- PHP 8.3 or newer
ext-mbstring- a Composer project with at least one
autoload.psr-4mapping
Project::open() reads the target project's composer.json and uses its PSR-4 mappings to locate and create PHP types.
Install
The 3.x branch is currently published as the development version 3.x-dev and has not yet been tagged as 3.0.0.
composer require small/class-manipulator:3.x-dev
After a stable 3.0 release is tagged, use:
composer require small/class-manipulator:^3.0
3.x API
use Small\ClassManipulator\Project;
$project = Project::open('/usr/src/my-app');
$class = $project->class(\App\Service\UserService::class);
$class
->final()
->implements(\App\Contract\UserServiceInterface::class);
$class
->property('repository')
->private()
->type(\App\Repository\UserRepository::class);
$method = $class
->method('find')
->public()
->returns(\App\Entity\User::class);
$method->parameter('id')->type('int');
$method->body('return $this->repository->find($id);');
$class->save();
class(), interface(), trait() and enum() load existing source. The matching createClass(), createInterface(), createTrait() and createEnum() methods create a new type at the path derived from Composer PSR-4 configuration.
Changes remain in memory until save() is called.
Members
method(), property(), constant() and parameter() use get-or-create semantics: an existing member is returned when its name already exists, otherwise it is added to the AST.
$class->property('enabled')
->private()
->type('bool')
->default(true);
$class->constant('VERSION')
->public()
->type('string')
->value('3.0');
$method = $class->method('enable')->public()->returns('void');
$method->body('$this->enabled = true;');
$class->save();
Types also expose imports, attributes, PHPDoc, methods and constants. Classes additionally expose properties, inheritance, implemented interfaces and used traits.
ClassFinder and queries
ClassFinder is a first-class 3.x API in the root Small\ClassManipulator namespace. It returns PhpType objects and delegates filtering to TypeQuery and ClassQuery.
$finder = $project->finder();
$all = $finder->all();
$classes = $finder->classes()->get();
$interfaces = $finder->interfaces()->get();
$traits = $finder->traits()->get();
$enums = $finder->enums()->get();
$services = $finder
->classes()
->inNamespace('App\\Service')
->implementing(\App\Contract\Service::class)
->withAttribute(\App\Attribute\Service::class)
->get();
Available query filters include:
inNamespace()for all type querieswithAttribute()for all type queriesextending()for class queriesimplementing()for class queries
Use Project::excludeNamespace() to omit a namespace tree from project discovery.
PHPDoc
$class->phpDoc()
->describe('Handles user lookup.')
->tag('template', 'T of User');
$class->method('find')->phpDoc()
->describe('Find one user.')
->param('int', 'id', 'User identifier.')
->returns(\App\Entity\User::class, 'Matching user.');
$class->property('repository')->phpDoc()
->var(\App\Repository\UserRepository::class);
Existing docblocks can be read and updated through description(), describe(), hasTag(), tagValues(), tag(), removeTag(), param(), returns(), var() and clear().
Creating types
$project->createClass(\App\Service\NewService::class)->save();
$project->createInterface(\App\Contract\NewServiceInterface::class)->save();
$project->createTrait(\App\Support\Reusable::class)->save();
$project->createEnum(\App\Status::class)->save();
Performance
SourceFile reuses the parser and pretty-printer across files, avoiding one parser instance per discovered PHP file.
Project discovery is intentionally not cached: each all() or query get() walks the configured PSR-4 directories and parses the current files from disk. This keeps results fresh when source files are changed outside the current Project instance.
When several filters apply to the same result set, chain them on one query before calling get() so the project is scanned once:
$services = $project
->finder()
->classes()
->inNamespace('App\\Service')
->implementing(\App\Contract\Service::class)
->withAttribute(\App\Attribute\Service::class)
->get();
2.x migration
3.x removes the legacy parser object graph and compatibility API, including ClassFile, ClassManipulator, the old nested ClassFinder filters/collections and Toolbox helpers.
Use Project, ClassFinder, TypeQuery / ClassQuery, and the AST-backed Type, Member, PhpDoc and Source APIs instead.
Development and quality gates
composer test runs both compact API tests and functional project workflows. The functional suite creates temporary Composer projects, writes generated PHP to disk, reloads it and executes generated classes.
PHPStan runs at level: max. Coverage is enforced at 100% for the production 3.x source surface.
composer test
composer phpstan
composer coverage
composer quality