Skip to content
shama edited this page Aug 23, 2012 · 22 revisions

Note This wiki page reflect my thoughts and don't indicate changes that will actually happen in CakePHP.

Some very experimental work towards this has begun. Please take a look here and tests.

Cake Bake

The baker is one the of things that originally attracted me to CakePHP. As useful as the tool is, I use it surprisingly very little; mostly for baking fixtures. I find it more work to remove the unwanted code baked from the core templates; so I build from scratch. I could build custom templates but I find that process to be a bit cumbersome and not mutable.

Problems

  • Bake templates are cumbersome
  • Baking cannot be done programmatically
  • You can only bake once... cannot rebake
  • Bake CLI is exhaustive
  • cough cough tests!

Ideas

Represent Code with Objects

Here is an example interface to bake a Controller:

<?php
// Read `app/Controller/PagesController.php` if it exists
$controller = new MetaClass('PagesController', 'Controller');
$controller
  ->method('admin_index', '$this->set("data", $this->paginate());')
  ->method('admin_edit', '$this->set("id", $id);', [
    'parameters' => [
      ['name' => 'id', 'type' => 'integer', 'default' => null]
    ],
  ])
  ->property('paginate' [
    'limit' => 25,
    'order' => ['Page.title' => 'asc'],
  ]);

// Merge with a core template preferring the code from $controller
$coreTemplateController = new MetaClass('PagesController', 'Core.Controller');
$controller = $coreTemplateController->merge($controller);

// Bake the controller
$controller->write();

An example meta class to represent a class could be:

Cake/Meta/MetaClass

<?php
namespace Cake\Meta;
class MetaClass {

  // To enable reading/writing in the CakePHP format
  use CodeFormat\CakePHP;

  public $name;
  public $properties = [];
  public $methods = [];

  // Array of Trait `MetaClass`'es
  public $uses = [];

  // Hold an instance of a MetaClass, more about this below...
  protected $_metaClass;

  // Class name and file location, similar to `App::uses()`
  public function __construct($name, $path) { }

  // Methods for creating methods and properties
  // If the first parameter is a Meta type, it will add instead of create
  // $options would contain: access, type, docblock and parameters for method
  public function method([MetaMethod $method | string $name, string $value, array $options]) { }
  public function property([MetaProperty $property | string $name, string $value, array $options]) { }

  // Method to merge methods/properties from one or more other MetaClass'es
  public function merge(MetaClass $class /*[, ... ]*/) { }
}

Cake/Meta/MetaProperty

<?php
namespace Cake\Meta;
class MetaProperty {
  use CodeFormat\CakePHP;
  public $name;
  public $type = 'string';
  public $value = 'Contents of the method here';
  public $docblock = ['@inheritDoc');
  public $access = 'public';
}

Cake/Meta/MetaMethod

<?php
namespace Cake\Meta;
class MetaMethod {
  use CodeFormat\CakePHP;
  public $name;
  public $parameters = [];
  public $value = 'Contents of the method here';
  public $docblock = ['@inheritDoc');
  public $access = 'public';

  public function parameter([MetaParameter $parameter, string $name, string $type, string $default, boolean $hasDefault]) { }
}

Cake/Meta/MetaParameter

<?php
namespace Cake\Meta;
class MetaParameter {
  use CodeFormat\CakePHP;
  public $name;
  public $default;
  public $type;
}

Why not extend ReflectionClass?

You can only define a class once in PHP. Holding an instance of MetaClass allows this to be more flexible. For example if you wanted to create a MetaClass, read/merge the properties of an existing class, modify and then merge in another class... extending ReflectionClass wouldn't allow this but an instance of MetaClass would. This makes this very testable and avoid hitting fatal errors.

Why Not Arrays?

I originally planned using arrays but objects have some advantages:

  • Using an object based API will produce cleaner looking code.
  • More reliable and modular. Using arrays we rely on the Hash class which will only work as long as the array structure is strictly enforced. This is not always the case with arrays.
  • Objects are easier to traverse.

Rebaking

If code is represented as an object then it can be manipulated. This affords re-baking. This allows a user to bake a class, modify that class then bake again without overwriting the changes. New functionality can be baked into old code. A single class can be built by merging multiple templates.

App Skeleton Templates

App skeletons could also be represented with objects:

<?php
class MetaApp {
  // Accept an array of an App structure
  public function __construct($structure = []) { }

  // Create the folders and files
  public function write() { }
}

This interface could be used like:

<?php
$app = new MetaApp([
  'Config' => [
    'Schema',
  ],
  'Controller' => [
    'Component',
    new MetaClass('PagesController', 'Controller'),
  ],
  'Model' => [
    'Behavior',
    new MetaClass('Page', 'Model'),
  ],
));

// Bake our App
$app->write();

What about Views (and other non-class files)?

We could create a MetaFile class to represent files and they could even be templated:

<?php
$data = [
  'title' => 'My New Project',
];
$app = new MetaApp([
  'View' => [
    'Pages' => [
      new MetaFile('admin_index.ctp', 'Core.View', $data),
    ],
  ],
  'webroot' => [
    'css' => [
      new MetaFile('cake.generic.css', 'Core.webroot/css', $data),
    ],
  ],
]);
$app->write();

Cake/Meta/MetaFile

<?php
namespace Cake\Meta;
class MetaFile {
  use CodeFormat\CakePHP;
  public function __construct($filename, $path, $data = []) { }
}

Remote Resources?

Possibility of detecting remote resources and using HttpSocket to download when building the app.

<?php
$app = new MetaApp([
  'webroot' => [
    'js' => [
      'jquery.min.js' => new MetaFile('https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'),
    ],
  ],
]);

Testable

Using the above methods enables bake templates and skeletons to become testable.

Bake API

Using a programatic method over flat file templates and folder structures allows bake to become an API. Users could then utilize and extend it.

Baking Traits

As @lorenzo pointed out in IRC: baking PHP 5.4 traits would be ideal. Traits would be a much cleaner method. Using a MetaClass to bake traits would still be useful as the traits could be rebaked and more easily tested.

An example to bake a trait could be:

<?php
$trait = new MetaClass('PageTrait', 'Trait');
$trait
  ->method('admin_index', '$this->set("data", $this->paginate());')
  ->method('admin_edit', '$this->set("id", $id);', [
    'parameters' => [
      ['name' => 'id', 'type' => 'integer', 'default' => null]
    ],
  ])
  ->property('paginate' [
    'limit' => 25,
    'order' => ['Page.title' => 'asc'],
  ]);

// Now we can create our Controller and use the above Trait
$controller = new MetaClass('PagesController', 'Controller');
$controller->traits($trait);

// Bake the controller and trait
$trait->write();
$controller->write();

Baking Plugins

As pointed out by @dereuromark, these methods would also solve plugin baking issues:

<?php
// Create a plugin model and behavior
$model = new MetaClass('Page', 'MyPlugin.Model');
$behavior = new MetaClass('MyBehavior', 'MyPlugin.Model/Behavior');

// This would bake to app/Plugin/MyPlugin/Model/Page.php
$model->write();

// Or create an entire plugin
$plugin = new MetaApp([
  'Plugin' => [
    'MyPlugin' => [
      'Model' => [
        'Behavior' => [$behavior],
        $model,
      ],
    ],
  ],
]);

// Bake the plugin
$plugin->write();

Config File Alternative to CLI

This would also allow a user to create a config file outlining how they would like their app baked. Then issue a single command like: cake bake Config/Bake/template.json As opposed to responding to the interactive questions or know the exhaustive list of commands to run.

Errors

What happens when we hit an error in baking? We don't want half-baked apps. This is the benefit of rebaking. We could correct the errors and rebake the app, merging with the half-baked app.

Another option is to provide a rollback() method to revert the changes in case of an error.

Code Standards

A CodeFormat trait could be created which does the actual code writing to a specific format. This class could be extended to modify the standards or formatting of the baked code.

<?php
namespace Cake\Meta\CodeFormat;
trait CakePHP {
  protected $_codeFormatOptions = [
    'indent' => "\t",
  ];

  // Will write the class/method/property/etc in the CakePHP format.
  public function write() {}

  // Will merge a class/method/property.
  public function merge() {}
}

Then a user could write their own code format to use other standards:

<?php
namespace Cake\Meta\CodeFormat;
trait PSR2 {
  protected $_codeFormatOptions = [
    'indent' => "    ",
  ];
}

Example

A very early example of this can be seen in Oven (uses arrays at the moment): https://github.com/shama/oven/blob/master/Lib/PhpBaker.php.

Thanks

Thanks to @rchavik for accidentally convincing me to use objects instead of arrays and @josegonzalez for letting me steal all his good ideas.