Skip to content

Commit 40c9b14

Browse files
authored
Merge pull request #33 from vanilla/add/muchos
Add a application boilerplate class
2 parents b0eb212 + 4e4863f commit 40c9b14

28 files changed

Lines changed: 2123 additions & 518 deletions

.travis.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
language: php
22

33
php:
4-
- 7.1
54
- 7.2
65
- 7.3
6+
- 7.4
77
- nightly
88

99
sudo: false
@@ -16,7 +16,10 @@ matrix:
1616
install:
1717
- composer install
1818

19-
script: ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover=coverage.clover
19+
script:
20+
- composer run phpcs
21+
- ./vendor/bin/psalm
22+
- ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover=coverage.clover
2023

2124
after_script:
2225
- wget https://scrutinizer-ci.com/ocular.phar

README.md

Lines changed: 161 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
![MIT License](https://img.shields.io/packagist/l/vanilla/garden-cli.svg?style=flat)
66
[![CLA](https://cla-assistant.io/readme/badge/vanilla/garden-cli)](https://cla-assistant.io/vanilla/garden-cli)
77

8+
- [Introduction](#introduction)
9+
- [Defining The CLI](#defining-the-cli)
10+
- [The CliApplication Class](#the-cliapplication-class)
11+
- [Logging](#logging)
12+
13+
## Introduction
14+
815
Garden CLI is a PHP command line interface library meant to provide a full set of functionality with a clean and simple api.
916

10-
## Why use Garden CLI?
17+
### Why use Garden CLI?
1118

1219
PHP's `getopt()` provides little functionality and is prone to failure where one typo in your command line options can wreck and entire command call. Garden CLI solves this problem and provides additional functionality.
1320

@@ -16,7 +23,7 @@ PHP's `getopt()` provides little functionality and is prone to failure where one
1623
* Have command options parsed and validated with error information automatically printed out.
1724
* A simple, elegant syntax so that even your most basic command line scripts will take little effort to implement robust parsing.
1825

19-
## Installation
26+
### Installation
2027

2128
*Garden CLI requires PHP 7.0 or higher*
2229

@@ -28,7 +35,11 @@ Garden CLI is [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accep
2835
}
2936
```
3037

31-
## Basic Example
38+
## Defining The CLI
39+
40+
The `Cli` class provides a fluent interface for defining commands, opts, and args.
41+
42+
### Basic Example
3243

3344
Here is a basic example of a command line script that uses Garden CLI to parse its options. Let's say you are writing a script called `dbdump.php` to dump some data from your database.
3445

@@ -61,7 +72,7 @@ This example returns an `Args` object or exits to show help or an error message.
6172
* If you want your option to have a short code then specify in with `name` argument separated by a colon.
6273
* If you specify a short code for an option this will act like an alias for the parameter name in `$argv` only. You always access an option by its full name after parsing.
6374

64-
## Option Types
75+
### Option Types
6576

6677
The `opt` method has a `$type` parameter that you can use to specify a type for the option. The valid types are `integer`, `string`, and `boolean` with string as the default.
6778

@@ -72,7 +83,7 @@ command --header="line1" --header="line2"
7283
7384
```
7485

75-
## Displaying Help
86+
### Displaying Help
7687

7788
If you were to call the basic example with a `--help` option then you'd see the following help printed:
7889

@@ -92,7 +103,7 @@ Dump some information from your database.
92103

93104
All of the options are printed in a compact table and required options are printed in bold. The table will automatically expand to accommodate longer option names and wrap if you provide extra long descriptions.
94105

95-
## Displaying Errors
106+
### Displaying Errors
96107

97108
Let's say you call the basic example with just `-P foo`. What you'd see is the following error message:
98109

@@ -102,7 +113,7 @@ Missing required option: database
102113
Missing required option: user
103114
</pre>
104115

105-
## Using the Parsed Options
116+
### Using the Parsed Options
106117

107118
Once you've successfully parsed the `$argv` using `Cli->parse($argv)` you can use the various methods on the returned `Args` object.
108119

@@ -115,7 +126,7 @@ $database = $args['database']; // use the args like an array too
115126
$port = $args->getOpt('port', 123); // get port with default 123
116127
```
117128

118-
## Multiple Commands Example
129+
### Multiple Commands Example
119130

120131
Let's say you are writing a git-like command line utility called `nit.php` that pushes and pulls information from a remote repository.
121132

@@ -147,32 +158,148 @@ Like the basic example, `parse()` will return a `Args` object on a successful pa
147158
* If the type of `opt()` is `integer` then you can count the number of times an option is supplied. In this example, this allowes you to specify multiple levels of verbosity by adding multiple `-v`s.
148159
* The `arg()` method lets you define arguments that go after the options on the command line. More on this below.
149160

150-
## Listing Commands
161+
### Listing Commands
151162

152163
Calling a script that has commands with no options or just the `--help` option will display a list of commands. Here is the output from the multiple commands example above.
153164

154165
<pre>
155-
<b>usage: </b>nit.php <command> [&lt;options&gt;] [&lt;args&gt;]
166+
<b>usage: </b>nit.php &lt;command&gt; [&lt;options&gt;] [&lt;args&gt;]
156167

157168
<b>COMMANDS</b>
158169
push Push data to a remote server.
159170
pull Pull data from a remote server.
160171
</pre>
161172

162-
## Args and Opts
173+
### Args and Opts
163174

164175
The `Args` class differentiates between args and opts. There are methods to access both opts and args on that class.
165176

166177
* Opts are passed by `--name` full name or `-s` short code. They are named and can have types.
167178
* Args are passed after the options as just strings separated by spaces.
168179
* When calling a script from the command line you can use `--` to separate opts from args if there is ambiguity.
169180

170-
## Formatting Output with the TaskLogger
181+
## The CliApplication Class
182+
183+
The basic `Cli` class works well for defining and documenting opts and args. However, you still need to wire up the parsed command line args to your own code. If you want to reduce this boilerplate, you can use the `CliApplication` class.
184+
185+
*Note: In order to use the `CliApplication` functionality you will need to require some extra dependencies. See the suggested packages in composer.json for more information.*
186+
187+
### Defining a Subclass of CliApplication
188+
189+
To use the `CliApplication` you usually subclass it and override the `configureContainer()` and `configureCli()` methods to define the commands in your app.
190+
191+
```php
192+
class App extends Garden\Cli\Application\CliApplication {
193+
protected function configureCli(): void {
194+
parent::configureCli();
195+
196+
// Add methods with addMethod().
197+
$this->addMethod('SomeClassName', 'someMethod');
198+
$this->addMethod('SomeClassName', 'someOtherMethod', [CliApplication::OPT_SETTERS => false]);
199+
$this->addMethod('SomeOtherClassName', 'someMethod', [CliApplication::OPT_COMMAND => 'command-name']);
200+
201+
// Add ad-hoc closures with addCallable().
202+
$this->addCallable('foo', function (int $count) { });
203+
204+
// Wire up dependencies with addConstructor() or addFactory().
205+
$this->addFactory(\PDO::class, [\Garden\Cli\Utility\DbUtils::class, 'createMySQL']);
206+
}
207+
208+
protected function configureContainer(): void {
209+
parent::configureContainer();
210+
211+
// Configure the container here.
212+
}
213+
}
214+
```
215+
216+
This example wires up three methods.
217+
218+
#### Using the `addMethod()` Method
219+
220+
You can wire up class methods to the command line by using `addMethod()`. This does the following:
221+
222+
1. It will create a command derived from the method name. Override the command name with the `OPT_COMMAND` option.
223+
2. It will create opts for object setters. Object setters are methods that start with the word `set` and take one argument. You can opt out of setter wiring with the `OPT_SETTERS` options.
224+
3. It will create opts for method parameters. If the method has class type-hinted types they will not be wired up to opts, but instead will be satisfied with the container.
225+
4. It will use method doc blocks to add descriptions for the command and opts. Make sure you use PHPDoc syntax.
226+
227+
You can call `addMethod()` with either a static or instance method. If you pass a static method then it will only wire up static setters. An instance method will wire up both static and instance methods.
228+
229+
#### Using the `addCallable()` Method
230+
231+
You can wire up an ad-hoc closure to the command line by using `addCallable()`. This works much like `addMethod()`, but will only reflect the callable's parameters.
232+
233+
Even though it's not a common practice to add a doc block to an inline closure, you can do so and it will be used to document the command. If you don't do so, but at least want a description then use the `OPT_DESCRIPTION` option to provide one.
234+
235+
#### Using the `addConstructor()` and `addFactory()` Methods
236+
237+
You can add dependencies by wiring up their constructor parameters or a factory method to the opts. The most common use case is specifying connection parameters to a database or an access token to an API client.
238+
239+
Use `addConstructor()` if the class has constructor parameters that make sense coming from the command line.
240+
241+
Use `addFactory()` if you want to clean up the names of the parameters or do some additional properties somehow.
242+
243+
If the constructor or factory has class type hints then not to worry. Those will be auto-wired through the container. You can then configure them through the container directory or even wire them up to opts by making additional calls to `addConstructor()` or `addFactory()`.
244+
245+
```php
246+
class App extends Garden\Cli\Application\CliApplication {
247+
protected function configureCli(): void {
248+
parent::configureCli();
249+
250+
// This will make the database connection get created by the DbUtils::createMySQL() method with command line opts for the same.
251+
$this->addFactory(\PDO::class, [\Garden\Cli\Utility\DbUtils::class, 'createMySQL']);
252+
$this->getContainer()->setShared(true);
253+
254+
// This will wire up the constructor parameters for the the StreamLogger to the command line and set is as the logger for the app.
255+
$this->addConstructor(\Garden\Cli\StreamLogger::class);
256+
$this->getContainer()->setShared(true);
257+
$this->getContainer()->rule(\Psr\Log\LoggerInterface::class)->setAliasOf(\Garden\Cli\StreamLogger::class);
258+
}
259+
}
260+
```
261+
262+
#### Using the `addCall()` Method
263+
264+
You can wire up a call to a class method using the `addCall()` method. Use this for setter injection. The call will be applied when the class is instantiated.
265+
266+
```php
267+
class App extends Garden\Cli\Application\CliApplication {
268+
protected function configureCli(): void {
269+
parent::configureCli();
270+
271+
// Wire up your github client's API key to the command line.
272+
$this->addCall(GithubClient::class, 'setAPIKey', [\Garden\Cli\Application\CliApplication::OPT_PREFIX => 'git-']);
273+
}
274+
}
275+
```
276+
277+
### Running Your Application
278+
279+
To use your application you just need to call the `main()` method.
280+
281+
```php
282+
$app = new App();
283+
$app->main($argv);
284+
```
285+
286+
The main method does the following:
287+
288+
1. Parses the `$argv` parameter to determine the command.
289+
2. If the command maps to an instance method then an instance is fetched from the container.
290+
3. Setters are applied from the opts.
291+
4. The method is invoked through the container, satisfying any arguments that were not specified as opts.
292+
293+
## Logging
294+
295+
Many CLI applications require some form of logging. Garden CLI has you covered.
296+
297+
### Formatting Output with the TaskLogger
171298

172299
The `TaskLogger` is a [PSR-3](https://www.php-fig.org/psr/psr-3/) log decorator helps you output task-based information to the console in a nice, compact style. It's good for
173300
things like install scripts, scripts that take a long time, or scripts you put into a cron job.
174301

175-
### Logging Tasks
302+
#### Logging Tasks
176303

177304
When using the `TaskLogger` you want to think in terms of messages and tasks. A message is a single log item to output
178305
to the user. A task has a begin and an end and can be nested as much as you want. Messages are output using the various PSR-3 methods while tasks are output with `begin()` and `end()`. Here are all of the methods you can use to log tasks.
@@ -185,40 +312,40 @@ to the user. A task has a begin and an end and can be nested as much as you want
185312
| `endError` | Log the end of a task that resulted in an error. |
186313
| `endHttpStatus` | Log the end of a task with an HTTP status. The log level is calculated from the number of the status. |
187314

188-
### Task Nesting and Durations
315+
#### Task Nesting and Durations
189316

190317
You can nest tasks as much as you wish by calling a `begin*` method before calling an `end*` method. Each time you nest a task it will output its messages indented another level. Tasks also calculate their duration and output it at after the call to `end`.
191318

192-
### Suppressing Messages
319+
#### Suppressing Messages
193320

194321
By default, the `TaskLogger` will only output messages that are at a level of `LogLevel::INFO` or higher. You can change this with the `setMinLevel` method. If you begin a task at a level that us suppressed, but a child message is at or above the min level then the begin task message will be output retroactively. This allows you to see what task kicked off the logged message.
195322

196-
### Example
323+
#### Example
197324

198325
```php
199326
$log = new TaskLogger();
200327

201-
$log->info('This is a message.')
202-
->error('This is an error.') // outputs in red
328+
$log->info('This is a message.');
329+
$log->error('This is an error.'); // outputs in red
203330

204-
->beginInfo('Begin a task')
205-
// code task code goes here...
206-
->end('done.')
331+
$log->beginInfo('Begin a task');
332+
// code task code goes here...
333+
$log->end('done.');
207334

208-
->beginDebug('Make an API call')
209-
->endHttpStatus(200) // treated as error or success depending on code
335+
$log->beginDebug('Make an API call');
336+
$log->endHttpStatus(200); // treated as error or success depending on code
210337

211-
->begin(LogLevel::NOTICE, 'Multi-step task')
212-
->info('Step 1')
213-
->info('Step 2')
214-
->beginDebug('Step 3')
215-
->debug('Step 3.1') // steps will be hidden because they are level 3
216-
->debug('Step 3.2')
217-
->end('done.')
218-
->end('done.');
338+
$log->begin(LogLevel::NOTICE, 'Multi-step task');
339+
$log->info('Step 1');
340+
$log->info('Step 2');
341+
$log->beginDebug('Step 3');
342+
$log->debug('Step 3.1'); // steps will be hidden because they are level 3
343+
$log->debug('Step 3.2');
344+
$log->end('done.');
345+
$log->end('done.');
219346
```
220347

221-
## The StreamLogger
348+
### The StreamLogger
222349

223350
If you create and use a `TaskLogger` object it will output nicely to the console out of the box. Under the hood it is using a `StreamLogger` object to handle the formatting of the tasks to an output stream, in this case stdout. You can replace or modify the `StreamLogger` if you want to control logging in a more granular level. Here are some options.
224351

@@ -230,7 +357,7 @@ If you create and use a `TaskLogger` object it will output nicely to the console
230357
| `setTimeFormat` | `'%F %T'` | Set the time format. This can be a `strftime` string or a callback. |
231358
| `setLevelFormat` | nothing | Set a callback to format a `LogLevel` constant. |
232359

233-
### Example
360+
#### Example
234361

235362
The following example creates a `StreamLogger` object and tweaks some of its settings before passing it into the `TaskLogger` constructor.
236363

@@ -248,7 +375,7 @@ $fmt->setTimeFormat(function ($ts) {
248375
$log = new TaskLogger($fmt);
249376
```
250377

251-
## Implementing Your Own Logger
378+
### Implementing Your Own Logger
252379

253380
You can give the `TaskLogger` any PSR-3 compliant logger and it will send its output to it. In order to use some of the special task functionality, you'll have to inspect the `$contenxt` argument of your `log` method. Here the fields that you may receive.
254381

composer.json

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,26 @@
99
}
1010
],
1111
"require": {
12-
"php": ">=7.1",
13-
"psr/log": "^1.0",
14-
"ext-json": "*"
15-
},
12+
"php": ">=7.2",
13+
"ext-json": "*",
14+
"psr/log": "^1.0"
15+
},
1616
"require-dev": {
17-
"phpunit/phpunit": "^7",
17+
"ergebnis/composer-normalize": "^2.8",
18+
"phpdocumentor/reflection-docblock": "^4.3",
19+
"phpunit/phpunit": "^8",
20+
"vanilla/garden-container": "^3.0",
1821
"vanilla/standards": "^1.3",
19-
"vimeo/psalm": "^3.4"
22+
"vimeo/psalm": "^3.16"
23+
},
24+
"suggest": {
25+
"ext-pdo": "Required for the DbUtils class.",
26+
"phpdocumentor/reflection-docblock": "Required for the CliApplication functionality.",
27+
"vanilla/garden-container": "Required for the CliApplication functionality."
2028
},
2129
"config": {
2230
"platform": {
23-
"php": "7.1"
31+
"php": "7.2"
2432
}
2533
},
2634
"autoload": {
@@ -34,6 +42,7 @@
3442
}
3543
},
3644
"scripts": {
45+
"phpcs": "phpcs --standard=./vendor/vanilla/standards/code-sniffer/Vanilla ./src",
3746
"psalter": "./vendor/bin/psalter --issues=MissingReturnType,MissingClosureReturnType,InvalidReturnType,InvalidNullableReturnType,InvalidFalsableReturnType,MissingParamType,MismatchingDocblockParamType,MismatchingDocblockReturnType,LessSpecificReturnType,PossiblyUndefinedVariable,PossiblyUndefinedGlobalVariable,UnusedProperty,UnusedVariable"
3847
}
3948
}

phpunit.xml.dist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
backupStaticAttributes="false"
55
colors="true"
66
convertErrorsToExceptions="true"
7-
convertNoticesToExceptions="false"
8-
convertWarningsToExceptions="false"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
99
processIsolation="false"
1010
stopOnFailure="false"
1111
bootstrap="vendor/autoload.php"

0 commit comments

Comments
 (0)