Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,62 @@ This repository holds the CLI functionality for [PHP.Gt/WebEngine](https://www.p

The following commands are exposed:

+ `gt add` - add a page, API endpoint or cron script from a template
+ `gt create` - create a new WebEngine application
+ `gt serve` - run the inbuilt development server
+ `gt build` - compile client-side assets
+ `gt cron` - invoke scripts or static functions at regular intervals
+ `gt run` - run all background scripts at once - a combination of `serve`, `build --watch` and `cron --watch --now`
+ `gt deploy` - instantly deploy your application to the internet

## `gt add`

Use `gt add type name [template]` to create new files in the current project.

Supported types:

+ `page`
+ `api`
+ `cron`

Examples:

+ `gt add page about`
+ `gt add api users`
+ `gt add cron cleanup`
+ `gt add page about multi-column`

`type` and `name` are required. `template` is optional.

### Built-in templates

If no template name is provided, `gt add` copies the built-in templates from this package:

+ `src/Template/page/template.html` -> `page/<name>.html`
+ `src/Template/page/template.php` -> `page/<name>.php`
+ `src/Template/api/template.php` -> `api/<name>.php`
+ `src/Template/api/template.json` -> `api/<name>.json`
+ `src/Template/cron/template.php` -> `cron/<name>.php`

If a template file contains `{{name}}`, it is replaced with the provided `name`.

### Project templates

If a template name is provided, files are loaded from the current working directory instead:

+ `<type>/_template/<template>.*`

For example:

+ `gt add page about multi-column`

This will copy:

+ `page/_template/multi-column.html` -> `page/about.html`
+ `page/_template/multi-column.php` -> `page/about.php`

The same lookup rule applies for `api`, `cron`, and future types that may be added later.

# Proudly sponsored by

[JetBrains Open Source sponsorship program](https://www.jetbrains.com/community/opensource/)
Expand Down
2 changes: 2 additions & 0 deletions bin/gt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<?php
use Gt\Cli\Application;
use Gt\Cli\Argument\ArgumentList;
use Gt\GtCommand\Command\AddCommand;
use Gt\GtCommand\Command\BuildCommand;
use Gt\GtCommand\Command\CreateCommand;
use Gt\GtCommand\Command\MigrateCommand;
Expand All @@ -21,6 +22,7 @@ foreach([ __DIR__ . "/../../..", __DIR__ . "/../vendor" ] as $vendor) {
$app = new Application(
"PHP.GT Command Line Interface",
new ArgumentList(...$argv),
new AddCommand(),
new CreateCommand(),
new RunCommand(),
new DeployCommand(),
Expand Down
3 changes: 2 additions & 1 deletion phpmd.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@
<property name="exceptions" value="a,b,i" />
</properties>
</rule>

<rule ref="rulesets/naming.xml/ShortMethodName">
<properties>
<property name="exceptions" value="or,do" />
<property name="exceptions" value="go,do" />
</properties>
</rule>
</ruleset>
195 changes: 195 additions & 0 deletions src/Command/AddCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php
namespace Gt\GtCommand\Command;

use Gt\Cli\Argument\ArgumentValueList;
use Gt\Cli\Command\Command;
use Gt\Cli\Parameter\NamedParameter;
use Gt\Cli\Stream;
use RuntimeException;

class AddCommand extends Command {
private const SUPPORTED_TYPES = [
"page",
"api",
"cron",
];

public function run(?ArgumentValueList $arguments = null):void {
$type = (string)$arguments?->get("type", "");
$name = (string)$arguments?->get("name", "");
$template = (string)$arguments?->get("template", "");

$this->assertValidType($type);
$this->assertValidName($name);

$templateFileMap = $this->getTemplateFileMap($type, $template);
$destinationFileMap = $this->getDestinationFileMap($type, $name, $templateFileMap);

$this->assertDestinationFilesDoNotExist($destinationFileMap);
$this->ensureDestinationDirectoryExists($type);
foreach($destinationFileMap as $sourcePath => $destinationPath) {
$this->copyTemplateFile($sourcePath, $destinationPath, $name);
}
}

public function getName():string {
return "add";
}

public function getDescription():string {
return "Add a page, API endpoint or cron script from a template";
}

public function getRequiredNamedParameterList():array {
return [
new NamedParameter("type"),
new NamedParameter("name"),
];
}

public function getOptionalNamedParameterList():array {
return [
new NamedParameter("template"),
];
}

public function getRequiredParameterList():array {
return [];
}

public function getOptionalParameterList():array {
return [];
}

private function assertValidType(string $type):void {
if(in_array($type, self::SUPPORTED_TYPES)) {
return;
}

$supportedTypes = implode(", ", self::SUPPORTED_TYPES);
$this->writeLine("Unknown add type '$type'. Supported types: $supportedTypes", Stream::ERROR);
exit(1); // phpcs:ignore
}

private function assertValidName(string $name):void {
if($name && !preg_match("/[^a-z0-9_-]/i", $name)) {
return;
}

$this->writeLine(
"Invalid name '$name'. Use only letters, numbers, hyphens and underscores.",
Stream::ERROR
);
exit(1); // phpcs:ignore
}

/** @return array<string, string> */
private function getTemplateFileMap(string $type, string $template):array {
if($template) {
return $this->getProjectTemplateFileMap($type, $template);
}

return $this->getBuiltInTemplateFileMap($type);
}

/** @return array<string, string> */
private function getBuiltInTemplateFileMap(string $type):array {
$templateDirectory = dirname(__DIR__) . "/Template/$type";
$templateFiles = glob($templateDirectory . "/template.*");

if(!$templateFiles) {
$this->writeLine("No built-in templates found for type '$type'.", Stream::ERROR);
exit(1); // phpcs:ignore
}

$fileMap = [];
foreach($templateFiles as $templateFile) {
$fileMap[$templateFile] = basename($templateFile);
}

return $fileMap;
}

/** @return array<string, string> */
private function getProjectTemplateFileMap(string $type, string $template):array {
$templateDirectory = getcwd() . "/$type/_template";
$templateFiles = glob($templateDirectory . "/" . $template . ".*");

if(!$templateFiles) {
$this->writeLine(
"Template '$template' was not found in $templateDirectory",
Stream::ERROR
);
exit(1); // phpcs:ignore
}

$fileMap = [];
foreach($templateFiles as $templateFile) {
$fileMap[$templateFile] = basename($templateFile);
}

return $fileMap;
}

/** @param array<string, string> $destinationFileMap */
private function assertDestinationFilesDoNotExist(array $destinationFileMap):void {
foreach($destinationFileMap as $destinationPath) {
if(file_exists($destinationPath)) {
$this->writeLine("Destination already exists: $destinationPath", Stream::ERROR);
exit(1); // phpcs:ignore
}
}
}

private function ensureDestinationDirectoryExists(string $type):void {
$destinationDirectory = getcwd() . DIRECTORY_SEPARATOR . $type;
if(!is_dir($destinationDirectory) && !mkdir($destinationDirectory, 0777, true)) {
$this->writeLine("Unable to create directory: $destinationDirectory", Stream::ERROR);
// TODO: Replace with proper exit code when upgrade to phpgt/cli v1.3.5
exit(1); // phpcs:ignore
}
}

private function copyTemplateFile(
string $sourcePath,
string $destinationPath,
string $name,
):void {
$contents = file_get_contents($sourcePath);
if($contents === false) {
throw new RuntimeException("Unable to read template file: $sourcePath");
}

$contents = str_replace("{{name}}", $name, $contents);
if(file_put_contents($destinationPath, $contents) === false) {
throw new RuntimeException("Unable to write file: $destinationPath");
}

$this->writeLine("Created $destinationPath");
}

/**
* @param array<string, string> $templateFileMap
* @return array<string, string>
*/
private function getDestinationFileMap(
string $type,
string $name,
array $templateFileMap
):array {
$destinationFileMap = [];

foreach($templateFileMap as $sourcePath => $sourceFileName) {
$extension = pathinfo($sourceFileName, PATHINFO_EXTENSION);
$destinationFileMap[$sourcePath] = getcwd()
. DIRECTORY_SEPARATOR
. $type
. DIRECTORY_SEPARATOR
. $name
. "."
. $extension;
}

return $destinationFileMap;
}
}
29 changes: 29 additions & 0 deletions src/Template/api/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"oneOf": [
{ "$ref": "#/$defs/request" },
{ "$ref": "#/$defs/response" }
],
"$defs": {
"request": {
"type": "object",

"properties": {
},

"required": [
]
},

"response": {
"type": "object",

"required": [
],

"properties": {
}
}
}
}
3 changes: 3 additions & 0 deletions src/Template/api/template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
function go():void {
}
3 changes: 3 additions & 0 deletions src/Template/cron/template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
function go():void {
}
2 changes: 2 additions & 0 deletions src/Template/page/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1>{{name}}</h1>
<p>This is the {{name}} page.</p>
3 changes: 3 additions & 0 deletions src/Template/page/template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
function go():void {
}
Loading