├── .editorconfig ├── .github └── workflows │ └── github-pages.yml ├── .gitignore ├── README.md ├── favicon.ico ├── package-lock.json ├── package.json ├── resources ├── all-the-things.png ├── background.webp ├── climate-animation.gif ├── count.webp ├── exit-12.jpg ├── scripts.js ├── spidey-sense.png ├── styles.css ├── tim-lytle.jpg └── whole-ass.gif ├── reveal-md.json └── slides.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/gaerfield/reveal-md-github-pages 2 | name: Public to GitHub Pages 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | - name: Cache node-modules 16 | uses: actions/cache@v2 17 | env: 18 | cache-name: cache-node-modules 19 | with: 20 | path: ~/.npm 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 22 | - name: Install and Build 23 | run: | 24 | npm install 25 | npm run build 26 | - name: Deploy 27 | uses: JamesIves/github-pages-deploy-action@releases/v3 28 | with: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | BRANCH: gh-pages 31 | FOLDER: build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | build 4 | node_modules 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building for the PHP Command Line Interface 2 | 3 | Executing PHP from the command line enables us to interact with our applications in new and interesting ways: from performing site maintenance to scaffolding new projects, CLI tools like [WP-CLI](http://wp-cli.org/), [Artisan](https://laravel.com/docs/5.1/artisan), and [Drush](http://www.drush.org/en/master/) make it easy to interface with our code without ever opening a browser. 4 | 5 | Attendees will be introduced to popular PHP CLI tools and their default capabilities. We'll discuss characteristics of good CLI scripts, strong use-cases for writing custom commands, then write several CLI programs across different platforms. 6 | 7 | :sparkles: **[View slides](https://stevegrunwell.github.io/building-for-php-cli)** :sparkles: 8 | 9 | This presentation also has [a companion repository, full of executable examples from this presentation](https://github.com/stevegrunwell/php-cli-examples). 10 | 11 | ### Demonstrated Tools 12 | 13 | * [Symfony Console Component](http://symfony.com/doc/current/components/console/introduction.html) 14 | * [PHP-CLI Tools](https://github.com/wp-cli/php-cli-tools) 15 | * [CLImate](https://climate.thephpleague.com/) 16 | 17 | ### Platform-specific CLI tools 18 | 19 | * [WP-CLI](https://wp-cli.org) 20 | * [Laravel Artisan](https://laravel.com/docs/master/artisan) 21 | * [Drush](https://www.drush.org) 22 | * [Joomlatools Console](https://www.joomlatools.com/developer/tools/console) 23 | 24 | ### Additional Resources 25 | 26 | * [Building PHP Daemons and Long Running Processes](https://prezi.com/pymsnzwlieqt/building-php-daemons-and-long-running-processes-tek15/) - Talk from php[tek] 2015 by [Tim Lytle](http://timlytle.net) 27 | * [Writing WP-CLI Commands That Work!](https://stevegrunwell.com/slides/wp-cli) - Sister talk focused on writing WP-CLI commands 28 | * [What are Exit Codes in Linux?](https://itsfoss.com/linux-exit-codes/) - Explanation of standard exit codes and their meanings 29 | * [Understanding Exit Codes and How to Use Them in Bash Scripts](http://bencane.com/2014/09/02/understanding-exit-codes-and-how-to-use-them-in-bash-scripts/) - Article by Benjamin Cane 30 | * [Flysystem](https://flysystem.thephpleague.com) - Popular package for filesystem operations across environments 31 | * [Cropping and Resizing Animated Gifs with Gifsicle](https://stevegrunwell.com/blog/cropping-resizing-gifsicle/) - Blog post explaining how to create animated thumbnails for gifs, which relies on calling [Gifsicle](https://www.lcdf.org/gifsicle/) via [`passthru()`](https://www.php.net/manual/en/function.passthru.php) 32 | 33 | ## Presentation History 34 | 35 | * [php[tek] 2024](https://tek.phparch.com) — April 24, 2024 ([Joind.in](https://joind.in/event/phptek-2024/building-for-the-php-command-line-interface), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/phptek-2024/slides.pdf)) 36 | * [Midwest PHP 2019](https://2019.midwestphp.org/) — March 8, 2019 ([Joind.in](https://joind.in/talk/b9a05), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/midwest-php/slides.pdf)) 37 | * [WavePHP 2018](https://wavephp.com/) — September 20, 2018 ([Joind.in](https://joind.in/talk/6908c), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/wavephp-2018/slides.pdf)) 38 | * [Southeast PHP](https://southeastphp.com/) - August 17, 2018 ([Joind.in](https://joind.in/talk/ed2e4), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/southeastphp-2018/slides.pdf)) 39 | * [PHPDetroit](https://phpdetroit.io/) - July 28, 2018 ([Joind.in](https://joind.in/talk/e6d00), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/phpdetroit-2018/slides.pdf)) 40 | * [php[tek] 2018](https://tek18.phparch.com/speakers/steve-grunwell/) - May 31, 2018 ([Joind.in](https://joind.in/talk/c6025), [PDF](https://github.com/stevegrunwell/building-for-php-cli/releases/download/phptek-2018/slides.pdf)) 41 | * [Music City Code 2017](https://www.musiccitycode.com/) - June 3, 2017 42 | * [CodeMash 2017](http://www.codemash.org/) - January 13, 2017 43 | * [Nomad PHP (EU)](https://nomadphp.com/nomadphp-2016-12-eu/) – December 15, 2016 ([Joind.in](https://joind.in/talk/dce28)) 44 | * [php[tek] 2016](https://tek16.phparch.com/speakers/#66432) – May 27, 2016 ([Joind.in](https://joind.in/talk/ce9a4)) 45 | * [Columbus PHP Meetup](http://www.meetup.com/phpphp/events/229434721/) – April 13, 2016 ([Joind.in](https://joind.in/talk/e9465)) 46 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-for-php-cli", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "git@github.com:stevegrunwell/building-for-php-cli.git" 7 | }, 8 | "devDependencies": { 9 | "open-cli": "^8.0.0", 10 | "reveal-md": "^6.1.0" 11 | }, 12 | "scripts": { 13 | "build": "reveal-md slides.md --static build --static-dirs resources --assets-dir \"\"", 14 | "dev": "reveal-md slides.md -w", 15 | "export": "open-cli http://localhost:1948/slides.md?print-pdf", 16 | "start": "reveal-md slides.md" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/all-the-things.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/all-the-things.png -------------------------------------------------------------------------------- /resources/background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/background.webp -------------------------------------------------------------------------------- /resources/climate-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/climate-animation.gif -------------------------------------------------------------------------------- /resources/count.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/count.webp -------------------------------------------------------------------------------- /resources/exit-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/exit-12.jpg -------------------------------------------------------------------------------- /resources/scripts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom scripting for Reveal.js 3 | */ 4 | 5 | /** 6 | * Construct a persistent footer. 7 | * 8 | * This is largely based on the reveal.js-titlebar package: 9 | * @link https://www.npmjs.com/package/reveal.js-titlebar 10 | */ 11 | const footer = document.createElement('footer'); 12 | footer.classList.add('presentation-footer'); 13 | footer.innerHTML = 'phpc.social/@stevegrunwell - stevegrunwell.com/slides/php-cli'; 14 | footer.setAttribute('hidden', true); 15 | document.getElementsByClassName('reveal')[0].appendChild(footer); 16 | 17 | /** 18 | * When changing to a slide with data-hide-footer, hide the presentation footer. 19 | * 20 | * @param Event event The Reveal event object. 21 | * 22 | * @return void 23 | */ 24 | const toggleFooter = e => { 25 | if (e.currentSlide.dataset.hasOwnProperty('hideFooter')) { 26 | footer.setAttribute('hidden', true); 27 | } else { 28 | footer.removeAttribute('hidden'); 29 | } 30 | } 31 | 32 | // Never show the footer in frame embeds 33 | if (window.self === window.top) { 34 | Reveal.on('ready', toggleFooter); 35 | Reveal.on('slidechanged', toggleFooter); 36 | } 37 | 38 | /** 39 | * Inject our custom highlight.js language (cli). 40 | * 41 | * This relies on the window.options variable defined in {@see reveal-md/lib/template/reveal.html}. 42 | */ 43 | if (typeof window.options !== undefined) { 44 | window.options.highlight.beforeHighlight = (hljs) => hljs.registerLanguage('cli', (hljs) => { 45 | return { 46 | name: 'CLI', 47 | case_insensitive: true, 48 | contains: [ 49 | hljs.HASH_COMMENT_MODE, 50 | hljs.QUOTE_STRING_MODE, 51 | // Bash prompt ($ or ~) 52 | { 53 | scope: 'title', 54 | match: /^\s*[\$~]\s+/, 55 | }, 56 | // Boolean operators (&&, ||) 57 | { 58 | scope: 'built_in', 59 | match: /(&&|\|\|)/ 60 | }, 61 | // Escape characters at the end of a line 62 | { 63 | scope: 'built_in', 64 | match: /\\s*$/, 65 | }, 66 | // Single pipes, redirection 67 | { 68 | scope: 'built_in', 69 | match: /[\|>]/, 70 | }, 71 | // Command options 72 | { 73 | scope: 'variable', 74 | match: /\s-{1,2}[a-z0-9-_]+=?/, 75 | }, 76 | ], 77 | }; 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /resources/spidey-sense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/spidey-sense.png -------------------------------------------------------------------------------- /resources/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom styles for Reveal.js. 3 | */ 4 | 5 | /* Fonts */ 6 | @import url(https://fonts.googleapis.com/css?family=Roboto+Slab:300,700;Roboto:700;Fira+Code:400); 7 | 8 | /* Override some defaults from the base theme. */ 9 | :root { 10 | --r-heading-font: Roboto, 'Source Sans Pro', sans-serif; 11 | --r-body-font: 'Roboto Slab', 'Source Sans Pro', sans-serif; 12 | --r-heading1-size: 1.6em; 13 | --r-heading1-text-shadow: 0; 14 | --r-code-font: 'Fira Code', monospace; 15 | --subdued-text-color: #555; 16 | } 17 | 18 | .reveal { 19 | font-family: var(--r-body-font); 20 | background: url('./background.webp') center center repeat; 21 | } 22 | 23 | /* Special styling for the title + thank you slides. */ 24 | .title-slide h1 { 25 | text-align: left; 26 | font-family: var(--r-code-font); 27 | font-weight: 100; 28 | text-transform: lowercase; 29 | word-spacing: -.25em; 30 | } 31 | 32 | .title-slide h1:before { 33 | content: '$'; 34 | display: inline-block; 35 | margin-right: .5em; 36 | font-weight: bold; 37 | color: var(--r-link-color); 38 | } 39 | 40 | .title-slide h1:after { 41 | content: '\25AF'; 42 | animation: blink 2s step-start infinite; 43 | font-size: calc(1em * var(--r-heading-line-height)); 44 | color: var(--subdued-text-color); 45 | } 46 | 47 | @keyframes blink { 48 | 50% { 49 | opacity: 0; 50 | } 51 | } 52 | 53 | .title-slide h1, 54 | .thank-you h2 { 55 | margin-bottom: 1em; 56 | } 57 | 58 | .byline { 59 | font-weight: bold; 60 | } 61 | 62 | .byline .role { 63 | display: block; 64 | font-size: .75em; 65 | font-weight: normal; 66 | } 67 | 68 | .reveal .slides-link { 69 | display: block; 70 | margin-top: 3rem; 71 | font-size: .75em; 72 | } 73 | 74 | .slides-link a { 75 | display: block; 76 | font-weight: 300; 77 | } 78 | 79 | /* Persistent footer */ 80 | .presentation-footer { 81 | --presentation-footer-color: var(--subdued-text-color); 82 | 83 | position: absolute; 84 | left: 0; 85 | right: 0; 86 | bottom: 0; 87 | z-index: 9999; 88 | padding: 0 5em .5em 1em; 89 | font-size: .5em; 90 | color: var(--presentation-footer-color); 91 | transition: .2s; 92 | } 93 | 94 | .presentation-footer[hidden] { 95 | display: none; 96 | } 97 | 98 | .presentation-footer a { 99 | display: inline-block; 100 | } 101 | 102 | @media (min-width: 600px) { 103 | .presentation-footer { 104 | font-size: .65em; 105 | } 106 | } 107 | 108 | /* Blockquotes and figures */ 109 | .reveal blockquote { 110 | width: 90%; 111 | box-shadow: none; 112 | } 113 | 114 | .reveal blockquote::before { 115 | content: '\201C'; 116 | position: absolute; 117 | top: .5em; 118 | left: -.3em; 119 | font-family: Georgia, serif; 120 | font-size: 2.25em; 121 | line-height: 0; 122 | color: var(--r-link-color-dark); 123 | } 124 | 125 | .reveal blockquote p:last-child { 126 | margin-bottom: 0; 127 | } 128 | 129 | .reveal cite, 130 | .reveal figure figcaption { 131 | font-size: .8em; 132 | color: var(--subdued-text-color); 133 | } 134 | 135 | .reveal cite:before { 136 | content: '—'; 137 | } 138 | 139 | /* Add a bit of extra space between top-level list items. */ 140 | .slides section > ol > li + li, 141 | .slides section > ul > li + li { 142 | margin-top: .5em; 143 | } 144 | 145 | /* Hide text that's hidden for the sake of presentation. */ 146 | .screen-reader-text { 147 | border: 0; 148 | clip: rect(1px, 1px, 1px, 1px); 149 | clip-path: inset(50%); 150 | height: 1px; 151 | margin: -1px; 152 | overflow: hidden; 153 | padding: 0; 154 | position: absolute; 155 | width: 1px; 156 | word-wrap: normal; 157 | } 158 | 159 | /* Pull image captions closer to the image */ 160 | .reveal .image-caption { 161 | margin-top: calc(-1 * var(--r-block-margin)); 162 | font-size: .75em; 163 | } 164 | 165 | /* Add .hide-line-numbers to code blocks in order to hide line numbering. */ 166 | .hide-line-numbers .hljs-ln-numbers { 167 | display: none; 168 | } 169 | 170 | /* Link to Tim's Building PHP Daemons and Long Running Processes talk. */ 171 | .beardhawk { 172 | display: flex; 173 | flex-direction: row; 174 | gap: .5em; 175 | align-items: center; 176 | max-width: 70%; 177 | margin: 0 auto; 178 | } 179 | 180 | .beardhawk img { 181 | max-width: 3em; 182 | border-radius: 50%; 183 | } 184 | 185 | /* Allow definition lists to be centered */ 186 | .reveal dl { 187 | margin-left: 0; 188 | text-align: center; 189 | } 190 | 191 | .reveal dl dd { 192 | margin-left: 0; 193 | } 194 | 195 | /* Extra space between elements in a definition list */ 196 | .reveal dl + p, 197 | dd + dt { 198 | margin-top: 1em; 199 | } 200 | 201 | /* Helper to disable text-transform: uppercase on headings */ 202 | .no-transform { 203 | text-transform: none; 204 | } 205 | 206 | /* Hide text that's hidden for the sake of presentation. */ 207 | .screen-reader-text { 208 | border: 0; 209 | clip: rect(1px, 1px, 1px, 1px); 210 | clip-path: inset(50%); 211 | height: 1px; 212 | margin: -1px; 213 | overflow: hidden; 214 | padding: 0; 215 | position: absolute; 216 | width: 1px; 217 | word-wrap: normal; 218 | } 219 | 220 | /* Don't bold types for variables */ 221 | code .typehint { 222 | font-weight: normal; 223 | font-style: italic; 224 | } 225 | 226 | /* Count von Count */ 227 | .count-von-count:after { 228 | content: ''; 229 | position: fixed; 230 | top: 40vh; 231 | display: block; 232 | width: 100%; 233 | height: 100vh; 234 | opacity: 0; 235 | background: url('count.webp') top center no-repeat; 236 | background-size: contain auto; 237 | transition: all .2s; 238 | } 239 | 240 | .count-von-count[data-fragment="2"]:after { 241 | opacity: 1; 242 | } 243 | 244 | /* Print styles */ 245 | @media print { 246 | /* When code highlighting, don't dim anything. */ 247 | .reveal .hljs.has-highlights tr { 248 | opacity: 1!important; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /resources/tim-lytle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/tim-lytle.jpg -------------------------------------------------------------------------------- /resources/whole-ass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevegrunwell/building-for-php-cli/a230379ce806943f4ea3e613be467c31b5733add/resources/whole-ass.gif -------------------------------------------------------------------------------- /reveal-md.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Building for the PHP Command Line Interface", 3 | "theme": "white", 4 | "highlightTheme": "a11y-dark", 5 | "css": [ 6 | "resources/styles.css" 7 | ], 8 | "scripts": [ 9 | "resources/scripts.js" 10 | ], 11 | "absoluteUrl": "https://stevegrunwell.github.io/building-for-php-cli/", 12 | "revealOptions": { 13 | "controls": true, 14 | "controlsTutorial": false, 15 | "pause": true, 16 | "pdfSeparateFragments": false, 17 | "progress": false, 18 | "slideNumber": false, 19 | "transition": "none" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /slides.md: -------------------------------------------------------------------------------- 1 | 2 | # Building for the PHP \\
Command Line Interface 3 | 4 | Steve Grunwell 5 | Staff Software Engineer, Mailchimp 6 | 7 | [@stevegrunwell@phpc.social](https://phpc.social/@stevegrunwell) 8 | [stevegrunwell.com/slides/php-cli](https://stevegrunwell.com/slides/php-cli) 9 | 10 | 11 | --- 12 | 13 | ## Why the CLI? 14 | 15 | Note: 16 | 17 | Before we talk about how, let's discuss *why* you might use PHP on the command line 18 | 19 | ---- 20 | 21 | ### PHP Everywhere! 22 | 23 | * Re-use application code 24 | * Reduce language sprawl 25 | * PHP ❤️ Scripting 26 | 27 | Note: 28 | 29 | * The biggest benefit of PHP on the CLI is that we're still working in PHP: 30 | * Speaking the same language as the rest of your application 31 | * No alternate implementations, duplicative services, etc. 32 | * Keeps codebase tighter and prevents every PHP dev on your team from *also* having to write Bash or Python 33 | * PHP is a scripting language at heart 34 | * The tools we use every day (Composer, PHP_CodeSniffer, PHPUnit, PHPStan, et al) are written in PHP and interacted with solely through the command line (no GUI required!) 35 | 36 | ---- 37 | 38 | ### Invoking PHP on the CLI 39 | 40 | Via the PHP binary: 41 | 42 | ```sh 43 | $ php my-command.php 44 | ``` 45 | 46 | 47 | With the PHP shebang: 48 | 49 | ```sh 50 | #!/usr/bin/env php 51 | ``` 52 | 53 | 54 | ```cli [1|2] 55 | $ chmod +x my-command.php 56 | $ ./my-command.php 57 | ``` 58 | 59 | 60 | Note: 61 | 62 | Two ways of running PHP scripts on the command line: 63 | 64 | 1. Explicitly passing the script as an argument to the `php` binary 65 | 2. Using the PHP shebang 66 | * Probably familiar if you've done shell scripting before 67 | * Tells the shell how to interpret the script (literally "php from the user's $PATH") 68 | * Only required if you want to be able to run it without explicitly calling the PHP binary 69 | 70 | As long as the script has an executable bit in its permissions, we can run it like any other command 71 | 72 | ---- 73 | 74 | ### When might I use them? 75 | 76 | * Data migrations & transformations 77 | * Maintenance scripts 78 | * Dev-only actions 79 | * Scaffolding 80 | * Other code changes 81 | * "#YOLO scripts" 82 | 83 | Note: 84 | 85 | Great places for PHP command line scripts include: 86 | 87 | * Data migrations, transformations, schema updates, table seeding, etc. 88 | * Maintenance scripts and scheduled jobs 89 | * Cron jobs, queues 90 | * Operations that are not meant to be customer facing 91 | * Scaffolding new models 92 | * Generating new migrations 93 | * YOLO scripts: scripts you're only going to run once (or a small number) of times. 94 | 95 | --- 96 | 97 | ## CLIs for your Favorite Frameworks 98 | 99 | Note: 100 | 101 | If you're working with a framework or CMS, chances are you already have the ability to talk to it via the CLI 102 | 103 | ---- 104 | 105 | ### [Drush](https://www.drush.org) 106 | 107 | * "Drupal Shell" 108 | * One of the OG CLI tools for PHP CMSs 109 | * Manage themes, modules, system updates, etc. 110 | 111 | Note: 112 | 113 | Credit where credit is due, Drush ("Drupal Shell") is one of the earliest CLI tools for managing a PHP application 114 | 115 | ---- 116 | 117 | ### [WP-CLI](https://wp-cli.org) 118 | 119 | * Install core, themes, plugins, etc. 120 | * Manage posts, terms, users, and more 121 | * Inspect and maintain cron, caches, and transients 122 | * Extensible for themes + plugins 123 | 124 | Note: 125 | 126 | Heavily inspired by Drush, WP-CLI lets you perform most operations on a WordPress site without touching the GUI: 127 | 128 | Before my current job, I spent five years working at a WordPress-oriented web host. We used WP-CLI for *everything*, including as part of our provisioning scripts 129 | 130 | ---- 131 | 132 | ### [Laravel Artisan](https://laravel.com/docs/master/artisan) 133 | 134 | * The underlying CLI for Laravel 135 | * Built atop the Symfony Console 136 | * Scaffold all the things 137 | * Allows packages to register new commands 138 | 139 | Note: 140 | 141 | * Artisan is the command line interface for Laravel 142 | * Built on top of Symfony Console (more in a minute) 143 | * Easily scaffold models, controllers, console commands, and more! 144 | * Third-party packages can register new commands 145 | 146 | ---- 147 | 148 | ### Joomlatools Console 149 | 150 | * CLI framework for Joomla 151 | * Manage sites, extensions, databases, etc. 152 | * Includes virtual host management 153 | 154 | Note: 155 | 156 | * Joomla counterpart of WP-CLI or Drush 157 | * Similar kinds of features: managing sites, users, extensions, etc. 158 | * Kind of neat: has the "vhost" command for managing Apache + nginx virtual hosts 159 | 160 | --- 161 | 162 | ## CLI Concepts 163 | 164 | Note: 165 | 166 | While it's not any more difficult than building anything else in PHP, there are some concepts that you need to understand if you're going to build for the CLI 167 | 168 | ---- 169 | 170 | ### Composability 171 | 172 | Good CLI commands should be **composable!** 173 | 174 | Note: 175 | 176 | Composability is one of the major tenents of *nix operating systems. 177 | 178 | Who can tell me what this means? 179 | 180 | ---- 181 | 182 | ### Rule of Composability 183 | 184 | > Developers should write programs that can communicate easily with other programs. This rule aims to allow developers to break down projects into small, simple programs rather than overly complex monolithic programs. 185 | 186 | Eric S. Raymond, [*The Art of Unix Programming*](https://en.wikipedia.org/wiki/The_Art_of_Unix_Programming) 187 | 188 | Note: 189 | 190 | Small programs that can communicate with each other through common interfaces (data streams) and be combined to do most anything 191 | 192 | ---- 193 | 194 | ### Data Streams 195 | 196 | Three default data streams: 197 | 198 | 0. STDIN - input 199 | 1. STDOUT - output 200 | 2. STDERR - errors 201 | 202 | Note: 203 | 204 | Think of a data stream as a channel that can be read from and/or written to. 205 | 206 | Generally, there are three data streams to concern yourself with: 207 | 208 | 1. STDIN represents the data coming into your command 209 | 2. STDOUT is where you're sending data out 210 | 3. STDERR is where we collect any error information 211 | 212 | Streams can be redirected (e.g. write errors to a log file, send the output of one command as the input into another) 213 | 214 | ---- 215 | 216 | ### Data Streams in Practice 217 | 218 | ```cli [|2-3|4|5|6|7] 219 | # Get the number of unique IP addresses in access.log 220 | $ grep -Eo "([0-9]{1,3}[\.]){3}[0-9]{1,3}" \ 221 | /var/log/nginx/access.log \ 222 | | uniq \ 223 | | wc -l \ 224 | | xargs printf "%d unique IP addresses detected" 225 | 43282 unique IP addresses detected 226 | ``` 227 | 228 | 229 | Note: 230 | 231 | Counting the number of IP addresses in access.log: 232 | 233 | 1. Use `grep` to match anything that looks like an IP, returning only that part 234 | * STDOUT would be a series of IP addresses, one per line 235 | 2. Pipe that list of addresses into `uniq` to remove duplicates 236 | * STDOUT from grep became STDIN to uniq 237 | 3. Pipe the filtered list into `wc` (word count) with the `-l` option (count the number of lines) 238 | * STDOUT becomes an integer representing the number of lines 239 | 4. Use `xargs` to append that number to printf to give a summary 240 | 241 | Five commands (grep, uniq, wc, xargs, and printf), each playing their part 242 | 243 | ---- 244 | 245 | ### Exit Codes 246 | 247 | Exit codes tell us how everything went: 248 | 249 | | Code | Meaning | 250 | | --- | --- | 251 | | 0 | All good! | 252 | | 1 | Generic error | 253 | | 2 | Incorrect command/arg usage | 254 | | 3–255 | Specific errors | 255 | 256 | Note: 257 | 258 | When a command exits, we do so with an exit code. 259 | 260 | * 0 means that no errors occurred 261 | * 1–255 represent some sort of error 262 | * 1 is generally a catch-all for errors 263 | * 2 is typically meant to indicate incorrect command/arg usage 264 | * 3–255 may have special meaning; there are a few conventions in the 120s for permissions errors 265 | * You might use 3 for filesystem issues, 4 for network connectivity issues, etc. 266 | 267 | Most scripts you come across will generally use 0 or 1: did it succeed or fail (respectively)? 268 | 269 | ---- 270 | 271 | ### Exit Codes & Boolean Operators 272 | 273 | ```cli [1-2|4-5|7-8|10-11] 274 | # Celebrate a non-zero exit code! 275 | $ do-something && celebrate 276 | 277 | # Hang your head in shame if something fails 278 | $ do-something || hang-head-in-shame 279 | 280 | # Put the operators together 281 | $ (do-something && celebrate) || hang-head-in-shame 282 | 283 | # Semi-colons don't care, they just separate commands 284 | $ do-something; celebrate; hang-head-in-shame 285 | ``` 286 | 287 | 288 | Note: 289 | 290 | We can chain operations based on the exit code of the previous command: 291 | 292 | * Double-ampersand ("and") will proceed if the previous operation had an exit code of zero 293 | * Double pipes ("or") will proceed if we encountered a non-zero exit code 294 | * Both can be used, but use parentheses if you want the "or" to be tied to the "and" sequence 295 | * Semi-colons can chain multiple commands with no attention paid to exit codes 296 | 297 | ---- 298 | 299 | ### Arguments + Options 300 | 301 | ```cli [1-3|5-8|10-12] 302 | # Arguments 303 | $ cd /var/www 304 | $ grep "Some text" file.txt 305 | 306 | # Options 307 | $ git commit -m "This is my commit message" 308 | $ ls -a -l 309 | $ ls -al 310 | 311 | # Long options 312 | $ composer outdated --format=json 313 | $ git push --force-with-lease 314 | ``` 315 | 316 | 317 | Note: 318 | 319 | * Arguments: positional parameters, passed in order 320 | * Options: Can optionally have values, single dash + single letter. Can usually be combined 321 | * e.g. `ls -a -l` is the same as `ls -al` 322 | * Long options: Same as regular options, but with two dashes + multiple letters 323 | * Often easier to read or decode at a glance 324 | 325 | ---- 326 | 327 | ### Conventions for Options 328 | 329 | ```plaintext 330 | OPTIONS: 331 | 332 | -h|--help Print usage instructions 333 | -q|--quiet Silence all output 334 | -v|--version Print version information 335 | --verbose Print additional output 336 | ``` 337 | 338 | Note: 339 | 340 | While these aren't mandatory, there are a few common patterns you'll come across: 341 | 342 | * Many scripts will reserve `-h` and/or `--help` for displaying usage instructions 343 | * `-q` or `--quiet` is generally used to silence output 344 | * Especially useful for commands that may be run on a cron job, where you only want output if something goes wrong 345 | * `-v` has two common uses: either as a short-hand for version or verbose (print additional information) 346 | 347 | Notice that most of these options have both short and long versions! 348 | 349 | ---- 350 | 351 | ### Environment Variables 352 | 353 | Set and read variables in the current environment 354 | 355 | ```cli [1-2|4-5|7-8|] 356 | # Export from shell files 357 | export CURRENT_CITY="Bowling Green" 358 | 359 | # Set directly in shell 360 | $ CURRENT_CITY="Chicago" 361 | 362 | # Set as you call a command 363 | $ CURRENT_CITY="Rosemont" some-script 364 | ``` 365 | 366 | 367 | Note: 368 | 369 | There are three ways to set environment variables: 370 | 371 | 1. Export them from within a file like `.bash_profile`, which is sourced as your start your shell 372 | * Persists for all sessions 373 | 2. Explicitly set the variable in the shell 374 | * Persists for remainder of session 375 | 3. Set them as you're calling a command 376 | * Only set for the single command invocation 377 | 378 | If I set it all three of these ways, what would some-script get for the value of CURRENT_CITY? (Rosemont) 379 | 380 | ---- 381 | 382 | ### Environment Variables in PHP 383 | 384 | ```php [1-2|4-5|7-8|10-11] 385 | # Get array of all environment variables 386 | getenv(); 387 | 388 | # Retrieve a specific variable (false if unset) 389 | getenv('SOMEVAR'); 390 | 391 | # Set an environment variable 392 | putenv('SOMEVAR=some_value'); 393 | 394 | # Delete an environment variable 395 | putenv('SOMEVAR='); 396 | ``` 397 | 398 | 399 | Note: 400 | 401 | There are two primary functions for working with environment variables in PHP: 402 | 403 | 1. `getenv()` reads from the environment variables 404 | 2. `putenv()` writes to the environment variables 405 | 406 | There's also the `$_ENV` superglobal, but writing to this array has no impact on the environment. 407 | 408 | ---- 409 | 410 | ### The cli SAPI 411 | 412 | Additional **S**erver **API** for PHP 413 | 414 | ```php 415 | // Check the current SAPI. We can also use PHP_SAPI here. 416 | if (php_sapi_name() === 'cli') { 417 | // We're on the command line!! 418 | } 419 | ``` 420 | 421 | Note: 422 | 423 | * PHP has a number of server APIs that can introduce alternate functionality; cli is one of them 424 | * Other SAPIs include apache, cgi-fcgi, fpm-fcgi, litespeed, phpdbg, etc. 425 | * We can determine what SAPI we're using with the `php_sapi_name()` function or `PHP_SAPI` constant. 426 | 427 | ---- 428 | 429 | ### Special CLI globals 430 | 431 |
432 |
int $argc
433 |
Argument count
434 |
array $argv
435 |
Argument values
436 |
437 | 438 | Both will always have at least one value! 439 | 440 | Note: 441 | 442 | The CLI SAPI exposes two CLI-specific global variables: argc and argv. 443 | 444 | * $argc tells us the number of arguments passed to the script 445 | * $argv is an array of those values 446 | 447 | These will never be empty, because the script name is the first argument (even if just "Standard input code") 448 | 449 | ---- 450 | 451 | #### What will we see? 452 | 453 | ```cli 454 | $ php -r 'echo "{$argc} arg(s):\n"; var_export($argv);' \ 455 | PHP "is great" 456 | ``` 457 | 458 | ``` 459 | 3 arg(s): 460 | array ( 461 | 0 => 'Standard input code', 462 | 1 => 'PHP', 463 | 2 => 'is great', 464 | ) 465 | ``` 466 | 467 | 468 | Note: 469 | 470 | To demonstrate argc and argv, let's pass a simple script to the CLI PHP interpreter: 471 | 472 | Can anyone guess the values of argc and argv? 473 | 474 | ---- 475 | 476 | ### Daemons 477 | 478 | A process that continually runs in the background 479 | 480 | ```php 481 | while (true) { 482 | // do something! 483 | } 484 | ``` 485 | 486 |
487 | Tim Lytle 488 |
Building PHP Daemons and Long Running Processes
489 |
490 | 491 | Note: 492 | 493 | * Not the best use of PHP, but useful for things like workers 494 | * Talk that really got me into PHP CLI: "Building PHP Daemons and Long Running Processes" by Tim Lytle 495 | * php[tek] 2015 496 | 497 | --- 498 | 499 | ## Writing CLI Commands 500 | 501 | [github.com/stevegrunwell/php-cli-examples](https://github.com/stevegrunwell/php-cli-examples) 502 | 503 | Note: 504 | 505 | Now that we have a foundation, let's get into writing our own commands! 506 | 507 | Sample repo available with these examples and more! 508 | 509 | ---- 510 | 511 | ### A simple greeter 512 | 513 | ```php [|4|6] 514 | #!/usr/bin/env php 515 | 522 | 523 | Note: 524 | 525 | Let's start with a bare-bones greeter script: 526 | 527 | * First we'll grab the first argument and, if not present, fall back to "there" 528 | * Then `printf()` "hello, $name" 529 | 530 | ---- 531 | 532 | ```cli 533 | $ php hello.php Ben 534 | Hello, Ben! 535 | ``` 536 | 537 | ```cli 538 | $ php hello.php 539 | Hello, there! 540 | ``` 541 | 542 | 543 | Note: 544 | 545 | In practice, our script works like this: 546 | 547 | Calling the script with "Ben" as an argument makes it say "Hello, Ben!" 548 | 549 | No argument means it falls back to "Hello, there!" 550 | 551 | ---- 552 | 553 | ### Accepting Options 554 | 555 | ```php [|8-10|11|12|14] 556 | #!/usr/bin/env php 557 | # 558 | # USAGE: 559 | # 560 | # hello.php [-g|--greeting=] 561 | 572 | 573 | Note: 574 | 575 | Let's take our script from earlier and let a custom greeting be passed via either `-g` or `--greeting`. 576 | 577 | * The `getopt()` function parses the given options out of `$argv` 578 | * `g:` means `-g` with a value 579 | * `greeting` means `--greeting`, also with a value 580 | * The third argument is a variable that will be set by reference and tell you where `getopt()` stopped parsing options 581 | * Since we're accepting `--greeting` and `-g`, one should take precedence if both are present 582 | * In this case, our greeting will be `--greeting` if present, otherwise `-g`. If no greeting is passed, default to "hello" 583 | * The arguments come after any options, so we'll take advantage of `$index` to determine where the actual name is passed 584 | * If we can't find one, default to "there" 585 | * Finally, print the greeting along with the name: 586 | 587 | ---- 588 | 589 | ```cli 590 | $ php hello.php --greeting="Salutations" Dylan 591 | Salutations, Dylan! 592 | ``` 593 | 594 | ```cli 595 | $ php hello.php -g="Salutations" Dylan 596 | Salutations, Dylan! 597 | ``` 598 | 599 | 600 | Note: 601 | 602 | Running the new version, we can pass `--greeting` or `-g` with an equal sign. If both are present, we'll favor `--greeting` 603 | 604 | However, the format can be rather restrictive: 605 | 606 | * All options must come before arguments 607 | * Messing up the $rest_index (third arg of `getopt()`, set by reference) means that option keys can easily slip in as values 608 | * No validation, so you have to handle that yourself 609 | 610 | ---- 611 | 612 | ### We can do better than `getopt()`! 613 | 614 | !["Exit 12 offramp" meme, with a car labeled "PHP Developers" swerving hard away from "using getopt()" onto the "literally anything else" offramp](resources/exit-12.jpg) 615 | 616 | Note: 617 | 618 | Honestly, `getopt()` is a pain to work with and, as a result, a pain to use scripts that use it. 619 | 620 | In a minute, we'll take a look at some libraries and frameworks we can use to make handling all of these things easier 621 | 622 | ---- 623 | 624 | ### Performing system operations 625 | 626 | 635 | 636 | Note: 637 | 638 | When writing CLI scripts, it's not uncommon to need to do something on the filesystem. 639 | 640 | * PHP has built-in functions for things like `chmod()`, `mkdir()`, and other common Unix operations 641 | * If you're using Flysystem, there are even more options for filesystem manipulation 642 | * PHP can also execute arbitrary system commands: 643 | 644 | ---- 645 | 646 | ### Calling other scripts 647 | 648 |
649 |
exec()
650 |
Execute, return the last line of output
651 |
Can capture full output as array, exit code
652 |
shell_exec()
653 |
Execute, return the full output as string
654 |
655 | 656 | Note: 657 | 658 | The most common ways you'll see PHP call other scripts 659 | 660 | * `exec()` lets us execute a command and capture both the exit code and each line of output into an array 661 | * `shell_exec()` will return the full output as a string 662 | * No exit code, but perhaps the easiest way to call another script 663 | * The same as wrapping the command in backticks 664 | 665 | ---- 666 | 667 | ### Calling other scripts 668 | 669 |
670 |
system()
671 |
Returns last line of output
672 |
Flushes buffer as it goes
673 |
passthru()
674 |
Best choice for binary files
675 |
676 | 677 | Note: 678 | 679 | * `system()` 680 | * Works the same as its C equivalent 681 | * Will attempt to flush the output buffer as it goes, but only returns the last line 682 | * Can also capture the exit code to a variable by reference 683 | * `passthru()` doesn't attempt to transform the output, so this is really useful when working within binary files like images, video, etc. 684 | * Link in slides' README explaining how I used it to generate animated thumbnails for gifs 685 | 686 | ---- 687 | 688 | 689 | 690 | Note: 691 | 692 | If the thought of executing arbitrary system commands sets off your security sense: congratulations, your instincts are dead-on! 693 | 694 | If we're going to call other system commands, we need to make sure that we're properly escaping everything, _especially_ if there's any user-provided input! 695 | 696 | ---- 697 | 698 | ### Escaping commands & arguments 699 | 700 |
701 |
escapeshellcmd()
702 |
Escape an entire command
703 |
escapeshellarg()
704 |
Escape an individual argument
705 |
706 | 707 | Note: 708 | 709 | There are two major functions you should be aware of: 710 | 711 | 1. `escapeshellcmd()` escapes any meta-characters that could be used to chain other commands 712 | 2. `escapeshellarg()` escapes individual arguments and options and should *always* be used with user data 713 | 714 | ---- 715 | 716 | ### Without escaping 717 | 718 | ```php 719 | $name = 'Larry && rm -rf /'; 720 | 721 | # Uh oh, $name isn't being escaped! 722 | exec('greet-user ' . $name); 723 | ``` 724 | 725 | ```text 726 | # You're about to have a very bad day... 727 | Hello, Larry! 728 | ``` 729 | 730 | 731 | Note: 732 | 733 | For example, imagine we have a greet-user script, which accepts a name (maybe from a database or user input) and spits out "Hello, $name!" 734 | 735 | If the `$name` is coming from an untrusted source (like $_POST data), we could easily inject and execute arbitrary commands on our system! 😬 736 | 737 | ---- 738 | 739 | ### With proper escaping 740 | 741 | ```php 742 | $name = 'Larry && rm -rf /'; 743 | 744 | # Escape the argument with escapeshellarg() 745 | exec('greet-user ' . escapeshellarg($name)); 746 | ``` 747 | 748 | ```text 749 | # Weird name, but no harm done 750 | Hello, Larry && rm -rf /! 751 | ``` 752 | 753 | 754 | Note: 755 | 756 | Same as before, but wrapping `$name` in `escapeshellarg()` 757 | 758 | The ampersands are escaped, so this just looks like a really weird name (but doesn't hose our system) 759 | 760 | --- 761 | 762 | ## Libraries & Frameworks 763 | 764 | Note: 765 | 766 | With the fundamentals out of the way, we can start looking at some of the available libraries and frameworks to make writing PHP for the CLI way nicer 767 | 768 | ---- 769 | 770 | ### [Symfony Console](https://symfony.com/doc/current/components/console.html) 771 | 772 | * CLI framework of choice 773 | * Handlers for input & output 774 | * Built-in help screen, validation 775 | * Born to be tested 776 | 777 | Note: 778 | 779 | Component from the Symfony framework 780 | 781 | * De facto tool for writing PHP CLI scripts 782 | * Powers Artisan, Composer, Behat, and more 783 | * Ships with methods for all sorts of input and output handling 784 | * Commands allow you to register accepted arguments and options, including validation 785 | * Will then generate a help screen automatically 786 | * Designed from the ground-up to be easily tested 787 | * Also integrates well with other Symfony components 788 | 789 | ---- 790 | 791 | #### Building a Symfony Console Command 792 | 793 | ```php [|1|3,6|4,7-9] 794 | namespace App\Command; 795 | 796 | use Symfony\Component\Console\Attribute\AsCommand; 797 | use Symfony\Component\Console\Command\Command; 798 | 799 | #[AsCommand(name: 'app:create-user')] 800 | class CreateUserCommand extends Command 801 | { 802 | // ... 803 | ``` 804 | 805 | 806 | Note: 807 | 808 | Each Symfony Console command is its own class, which extends `Symfony\Component\Console\Command\Command` 809 | 810 | 1. First, define our namespace (we'll just use `App\Command`) 811 | 2. Set the command name (app:create-user) using the AsCommand attribute 812 | * Newer feature, can also be set in `configure()` method 813 | 4. Construct the class, extending that base Command 814 | 815 | ---- 816 | 817 | #### Configuring the command 818 | 819 | ```php 820 | protected function configure(): void 821 | { 822 | $this->setDescription('Creates a new user.') 823 | ->setHelp(/* Full help text goes here... */) 824 | ->addArgument(/* ... */) 825 | ->addOption(/* ... */); 826 | } 827 | ``` 828 | 829 | Note: 830 | 831 | * The `configure()` method lets us set things like the description, help text, and define any arguments and/or options our command might take. 832 | * Inputs can be specified as required or optional, be configured to support multiple values, and even given defaults. 833 | * If we didn't use the `AsCommand` attribute, we could set the command name here 834 | 835 | ---- 836 | 837 | #### The execute() method 838 | 839 | ```php [1-7,11|10] 840 | use Symfony\Component\Console\Input\InputInterface; 841 | use Symfony\Component\Console\Output\OutputInterface; 842 | 843 | protected function execute( 844 | InputInterface $input, 845 | OutputInterface $output 846 | ): int { 847 | // Do something in here! 848 | 849 | return Command::SUCCESS; 850 | } 851 | ``` 852 | 853 | 854 | Note: 855 | 856 | * The main entry point for your command is the `execute()` method. 857 | * Receives implementations of the `InputInterface` and `OutputInterface` interfaces 858 | * Input lets us retrieve arguments and options 859 | * Output lets is write to the console, and includes ways to color output, format in different ways, and more 860 | * Method returns an exit code 861 | * Three exit code constants available on Command class: `Command::SUCCESS` (0), `Command::ERROR` (1), `Command::INVALID` (2) 862 | 863 | ---- 864 | 865 | #### Arguments + options 866 | 867 | ```php [1|3-5|7] 868 | $user = new User($input->getArgument('email')); 869 | 870 | if ($input->getOption('admin')) { 871 | $user->makeAdmin(); 872 | } 873 | 874 | $user->save(); 875 | ``` 876 | 877 | 878 | Note: 879 | 880 | Within `execute()`, we can create our new user. 881 | 882 | * Let's say our `User` model accepts an email address in its constructor: 883 | * We can retrieve this via `$input->getArgument('email')` because we registered it in `configure()` 884 | * If this argument has been marked required, Symfony will have already thrown an error before getting to this point 885 | * If the `--admin` option is present, we might call `$user->makeAdmin()` 886 | * Finally, call `$user->save()` to persist our model to the database 887 | 888 | ---- 889 | 890 | #### Bootstrap our command(s) 891 | 892 | ```php [|7-9|6,10|11] 893 | #!/usr/bin/env php 894 | add(CreateUserCommand()); 903 | $app->run(); 904 | ``` 905 | 906 | 907 | Note: 908 | 909 | A Symfony command by itself doesn't do much, it needs to be registered within a Symfony console application. 910 | 911 | Think of this like a video game console: the app is our Nintendo/Xbox/Playstation, while each command is a game in our library. 912 | 913 | This is essentially what the main Composer and Artisan files look like: 914 | 915 | * Create a new application 916 | * Register our `CreateUserCommand` 917 | * Call `run()` 918 | 919 | ---- 920 | 921 | #### Calling our command 922 | 923 | ```cli [1|3-4|6-7] 924 | $ php console.php app:create-user beth@example.com --admin 925 | 926 | # If we've made console.php executable 927 | $ console.php app:create-user andy@example.com 928 | 929 | # Produce the help documentation 930 | $ php console.php app:create-user --help 931 | ``` 932 | 933 | 934 | Note: 935 | 936 | Assuming we've named our bootstrap file "console.php", we can now call our new command in a few ways: 937 | 938 | 1. We can pass the filename to the PHP binary, and create a User for Beth with admin privileges 939 | 2. If we've made console.php executable, we can just call console.php directly 940 | 3. We can add the `--help` option, which will automatically generate help docs for us 941 | 942 | ---- 943 | 944 | ### [PHP-CLI Tools](https://github.com/wp-cli/php-cli-tools) 945 | 946 | * Maintained by the WP-CLI team 947 | * Simplify input + output 948 | * Prompts, menus, and more 949 | * Output formatting: tables, trees,
progress bars, and more! 950 | 951 | Note: 952 | 953 | * Library full of helper functions maintained by the WP-CLI team 954 | * Functions to handle input + output: 955 | * Prompt users for data or present menus of options 956 | * Formatters for coloring text, plus more advanced formats like tables or trees 957 | * Includes progress indicators 958 | 959 | ---- 960 | 961 | #### PHP-CLI Tools 962 | 963 | ```php [|6|7-8|10-12] 964 | #!/usr/bin/env php 965 | 978 | 979 | Note: 980 | 981 | An example program using PHP-CLI tools: 982 | 983 | * Ask how high we should count (with a default of 10) 984 | * Prompt "shall I shout it?", which accepts y or n (yes or no) 985 | * Depending on the value, assign either an exclamation mark or period 986 | * Then, from 1 until we reach the value of $limit, print out the number with the given suffix 987 | 988 | ---- 989 | 990 | 991 | #### PHP-CLI Tools 992 | 993 | ```cli [1|2|3|4-9] 994 | $ php Counter.php 995 | How high should I count? [10]: 5 996 | Shall I shout it? [y/N]y 997 | 1! 998 | 2! 999 | 3! 1000 | 4! 1001 | 5! 1002 | ``` 1003 | 1004 | 1005 | ---- 1006 | 1007 | ### [CLImate](https://climate.thephpleague.com/) 1008 | 1009 | * The League of Extraordinary Packages 1010 | * More focused on output 1011 | * Progress bars, borders, JSON, and more 1012 | * Includes helpers for ASCII art and animations! 1013 | !["Oh Hello" as animated ASCII art, rising from the bottom of the screen](resources/climate-animation.gif) 1014 | 1015 | Note: 1016 | 1017 | * Maintained by The League of Extraordinary Packages 1018 | * More output options than PHP-CLI Tools 1019 | * Includes some experimental inputs, including radio buttons and check-boxes 1020 | * Favorite feature: support for animations and ASCII art 1021 | 1022 | --- 1023 | 1024 | ## CLI Best Practices 1025 | 1026 | Note: 1027 | 1028 | As we wrap up, I'd like to share a few pieces of advice as you enter the world of building for the PHP CLI 1029 | 1030 | ---- 1031 | 1032 | ### Check Your Assumptions 1033 | 1034 | * Check that commands exist before using them 1035 | * Don't hard-code system paths 1036 | 1037 | Note: 1038 | 1039 | Everybody's machine is different, and you don't want your script to fail because someone has a different implementation of grep. 1040 | 1041 | If you remember from the shebang, we use `/usr/bin/env` to get the path to the PHP binary from the environment. Even on the same platform, different versions or installation methods may install to different spots. 1042 | 1043 | Good example: the location of where Homebrew installs things varies between Apple Silicon and Intel chips 1044 | 1045 | ---- 1046 | 1047 | ### Rule of Silence 1048 | 1049 | > Developers should design programs so that they do not print unnecessary output. This rule aims to allow other programs and developers to pick out the information they need from a program's output without having to parse verbosity. 1050 | 1051 | Eric S. Raymond, [*The Art of Unix Programming*](https://en.wikipedia.org/wiki/The_Art_of_Unix_Programming) 1052 | 1053 | Note: 1054 | 1055 | The amount of output will vary depending on the purpose of your script: some scripts give little to no feedback, while others just barf all over the console. The trick is to get your default output level _just right_. 1056 | 1057 | * A major platform migration might call for very detailed output 1058 | * A maintenance script may only need to print something if there was an error. 1059 | 1060 | ---- 1061 | 1062 | ```cli [1-3|5-6|8-12] 1063 | # Default behavior 1064 | $ some-command 1065 | Command completed successfully! 1066 | 1067 | # Only produce output if something went wrong 1068 | $ some-command --quiet 1069 | 1070 | # Be more verbose 1071 | $ some-command --verbose 1072 | Reindexing database...OK 1073 | Reticulating splines...OK 1074 | Command completed successfully! 1075 | ``` 1076 | 1077 | 1078 | Note: 1079 | 1080 | Know your audience, and only print the bare minimum by default. Use options like --verbose for when users need more. 1081 | 1082 | ---- 1083 | 1084 | ### Garbage Collection 1085 | 1086 | * Clean up objects when you're done 1087 | * Be judicious with caching 1088 | * Watch for ballooning objects & arrays! 1089 | 1090 | Note: 1091 | 1092 | Not something we normally need to think about in PHP 1093 | 1094 | The garbage collector frees up memory that was previously allocated but no longer needed. This is normally handled by PHP automatically @ end of request 1095 | 1096 | Big difference between a 2s request and a 24hr command execution: 1097 | 1098 | * Help the garbage collector by explicitly unsetting variables 1099 | * Done with an object? Call `unset()` to hint to the gc that this can be cleaned 1100 | * Cache everything you can... 1101 | * But be aware that if (for example) you're tracking the results of each record changed these arrays can get **huge** 1102 | * Determine a reasonable batch size and reset things once you reach that number 1103 | * Maybe write out details to a log file, then reset the array 1104 | 1105 | ---- 1106 | 1107 | ### Ignore Web Requests 1108 | 1109 | If your commands live within the web root, prevent them from being run outside the CLI! 1110 | 1111 | ```php 1112 | // Only allow this script to run on the CLI! 1113 | if (PHP_SAPI !== 'cli') { 1114 | exit; 1115 | } 1116 | ``` 1117 | 1118 | Note: 1119 | 1120 | Modern frameworks keep most app code out of the web root, but if you're writing commands that will live under the web root **be sure that they can't be executed by a web request!** 1121 | 1122 | Exit code doesn't really matter here, you just don't want it to run. 1123 | 1124 | ---- 1125 | 1126 | ### Swanson on Commands 1127 | 1128 | ![Ron Swanson (Nick Offerman) advising "Never half-ass two things. Whole-ass one thing."](resources/whole-ass.gif) 1129 | 1130 | Note: 1131 | 1132 | A common mistake is trying to build a single CLI command that can do it all. 1133 | 1134 | Remember composability: build small, single-purpose commands and then use those to compose sophisiticated pipelines 1135 | 1136 | --- 1137 | 1138 | 1139 | 1140 | ## Thank You! 1141 | 1142 | Steve Grunwell 1143 | Staff Software Engineer, Mailchimp 1144 | 1145 | [@stevegrunwell@phpc.social](https://phpc.social/@stevegrunwell) 1146 | [stevegrunwell.com/slides/php-cli](https://stevegrunwell.com/slides/php-cli) 1147 | [github.com/stevegrunwell/php-cli-examples](https://github.com/stevegrunwell/php-cli-examples) 1148 | 1149 | 1150 | Note: 1151 | 1152 | REMEMBER TO REPEAT THE QUESTION!! 1153 | --------------------------------------------------------------------------------