ajthinking / archetype by ajthinking

Programmatically edit PHP and Laravel files.
1,391,103
245
5
Package Data
Maintainer Username: ajthinking
Maintainer Contact: jurisoo@hotmail.com (Anders Jürisoo)
Package Create Date: 2020-01-03
Package Last Update: 2023-02-11
Home Page:
Language: PHP
License: MIT
Last Refreshed: 2024-03-28 03:17:00
Package Statistics
Total Downloads: 1,391,103
Monthly Downloads: 63,908
Daily Downloads: 2,675
Total Stars: 245
Total Watchers: 5
Total Forks: 19
Total Open Issues: 2

php::archetype(:fire::fire::fire:);

Enabling Rapid-Application-Development-tools, PR-bots, code analyzers and other things

tests version

  • Programatically modify PHPFiles and LaravelFiles with an intuiutive top level read/write API
  • Read/write on classes, framework- and language constructs using FileQueryBuilders and AbstractSyntaxTreeQueryBuilders
  • Extract Application entity schemas
  • Add Snippets with an inline PHP Template engine

Table of Content

Installation

composer require ajthinking/archetype

Requires UNIX filesystem, PHP >= 7 and Laravel >= 7

Usage

PHPFile read/write API

use PHPFile;

// Create new files
PHPFile::make()->class('acme/Product.php')
    ->use('Shippable')
    ->add()->trait('Acme\Traits\Shippable')
    ->public()->property('stock', -1)
    ->save();
// Modify existing files  
PHPFile::load('app/Models/User.php')
    ->className('NewClassName')
    ->save();

LaravelFile read/write API

use LaravelFile; // extends PHPFile

// Expanding on our User model
LaravelFile::user()
    ->add()->use(['App\Traits\Dumpable', 'App\Contracts\PlayerInterface'])
    ->add()->implements('PlayerInterface')
    ->add()->trait('Dumpable')
    ->table('gdpr_users')
    ->add()->fillable('nickname')
    ->remove()->hidden()
    ->empty()->casts()
    ->hasMany('App\Game')
    ->belongsTo('App\Guild')
    ->save();

Result:

Review full API documentation here :point_left:

File QueryBuilder

Filter and retrieve a set of files to interact with.

// find files with the query builder
PHPFile::in('database/migrations')
    ->where('extends', 'Migration')
    ->andWhere('className', 'like', 'Create')
    ->get() // returns Collection of PHPFiles

// Quickly find the Laravel User file
$file = LaravelFile::user();

// Quickly find Laravel specific files
LaravelFile::models()->get();
LaravelFile::controllers()->get();
LaravelFile::serviceProviders()->get();
// ...

See a few more QueryBuilder examples in the tests :point_left:

Abstract Syntax Tree QueryBuilder

As seen in the previous examples we can query and manipulate nodes with simple or primitive values, such as strings and arrays. However, if we want to perform custom or more in dept queries we must use the ASTQueryBuilder.

Example: how can we fetch explicit column names in a migration file?

LaravelFile::load('database/migrations/2014_10_12_000000_create_users_table.php')
    ->astQuery() // get a ASTQueryBuilder

    ->method()
        ->where('name->name', 'up')
    ->staticCall()
        ->where('class', 'Schema')
        ->where('name->name', 'create')
    ->args
    ->closure()
    ->stmts
    ->methodCall()
        ->where('var->name', 'table')
    ->args
	->value
	->value
	->get(); // exit ASTQueryBuilder, get a Collection        

The ASTQueryBuilder examines all possible paths and automatically terminates those that cant complete the query:

The ASTQueryBuilder relies entirely on nikic/php-parser. Available query methods mirror the PhpParser types and properties. To understand this syntax better you may want to tinker with dd($file->ast()) while building your queries. Basic conventions are listed below.

  • Traverse into nodes by using methods (method(),staticCall() ...)
  • Traverse into node properties by accessing properties (args,stmts ...)
  • Filter results with where(...)
  • Resolving matching paths with get()

ASTQueryBuilder also supports removing, replacing and injecting nodes :wrench:

// Replace a node property
$file->astQuery()
    ->class()
    ->name
    ->replaceProperty('name', $newClassName)
    ->commit() // updates the file's AST
    ->end() // exit query
    ->save() 

More ASTQueryBuilder examples here :point_left:

Laravel schema

Use the LaravelSchema class to get an app schema.

use Archetype\Schema\LaravelSchema;

LaravelSchema::get();
{
    "entities": [
        {
            "model": "App\\User",
            "table": "users",
            "columns": {
                "id": {
                    "name": "id",
                    "type": {},
                    "default": null,
                    "notnull": true,
                    "length": null,
                    "precision": 10,
                    "scale": 0,
                    "fixed": false,
                    "unsigned": false,
                    "autoincrement": true,
                    "columnDefinition": null,
                    "comment": null
                },
                "name": {
                    "name": "name",
                    "type": {},
                    "default": null,
                    "notnull": true,
                    "length": null,
                    "precision": 10,
                    "scale": 0,
                    "fixed": false,
                    "unsigned": false,
                    "autoincrement": false,
                    "columnDefinition": null,
                    "comment": null,
                    "collation": "BINARY"
                }
            }
        }
    ],
    "strategy_used": "Archetype\\Schema\\Strategies\\FromDatabase",
    "log": []
}

Schema feature is under construction ⚠

Template engine

Let's make a snippet for a method we want to insert. Start by creating a file storage/archetype/snippets/my-stuff.php like shown below. In the file, we put our template code including any encapsuling constructs (in our case we will have to put a class since methods only exists inside classes). Name anything you want to be configurable with a handle for instance '___TARGET_CLASS___'. Even your snippet name itself may be a handle as long as it is unique.

<?php

/**
 * Optionally use FAKE names to silence IDE warnings
 */
use Archetype\Support\FakeName; 
use Archetype\Support\FakeName as ANY;
use Archetype\Support\FakeName as ___TARGET_CLASS___;

/**
 * This is just a placeholder class where we can add our snippets
 */
class _ extends FakeName
{
    /**
    * ___DOC_BLOCK___
    */
    public function mySpecialMethod($arg)
    {
        $want = abs($arg);
        return $this->doSomethingWith(___TARGET_CLASS___::class, 'my template')
            ->use(ANY::thing(new static('you' . $want)));
    }    
}

Your snippet is then instantly available anywhere in your code:

use Archetype\Support\Snippet;

// Get the snippet
Snippet::mySpecialMethod()

// Pass an array of replacement pairs to replace any handles:
Snippet::mySpecialMethod([
    '___DOC_BLOCK___' => 'Inserted with archetype :)',
    '___TARGET_CLASS___' => 'App\Rocket'
]);

// Integrated example
PHPFile::load('app/Models/User.php')
    ->addMethod(
        Snippet::mySpecialMethod([
            // replacement pairs ...
        ])
    )->save();

:information_source: The Snippet class currently only supports templates on class methods.

A note on Facades

You may use either of the following

// Using class
(new \Archetype\PHPFile)->load('...');

// Using facade
PHPFile::load('...');

// Using facade explicitly
use Archetype\Facades\PHPFile;
PHPFile::load('...'); // Using facade explicitly

Errors 😵

If a file can't be parsed, a FileParseError will be thrown. This can happen if you try to explicitly load the file but also when performing queries matching problematic files.

To see all offending files run php artisan archetype:errors. To ignore files with problems, put them in config/archetype.php -> ignored_paths.

Limitations / Missing features

In general this package assumes code to be parsed follows guidellines and conventions from PSR and Laravel. Some examples are listed below.

  • Requires UNIX based file system - no windows support

  • Can't use group use syntax (use Something\{X, Y};)

  • Assumes one class per file

  • Assumes no multiple/grouped property declarations (protected $a, $b = 1;)

Configuration

php artisan vendor:publish --provider="Archetype\ServiceProvider"

Contributing

Development installation

git clone git@github.com:ajthinking/archetype.git
cd archetype
composer install
./vendor/bin/phpunit tests

License

MIT

Acknowledgements

Like this package?

Star it :star:

Say hi: @ajthinking :gem:

Github Sponsors :octocat::heart: