deSymfony 30 junio - 1 julio 2017
Castellón
APLICACIONES CLI
PROFESIONALES CON SYMFONY
Raúl Fraile
deSymfony
¡Muchas gracias a nuestros
patrocinadores!
$	whoami
Raúl	Fraile
Desarrollador	PHP	/	Symfony	-	Formador
6. adj. Hecho por alguien cuidando los detalles.
$ telnet mapscii.me
$ curl wttr.in/Castellon
github.com/lastguest/pixeler
curl --data '<document><page><item col="0" row="0" colSpan="5"
rowSpan="4"><bar maxHeight="5" data-titles="A,B,C" data-data="2,5,3" /></item></
page></document>' tty.zone?cols=$((COLUMNS))
$	nc	towel.blinkenlights.nl	23
Distribución
CaracterísFcas	
profesionales
Extensibilidad
Portabilidad
symfony/console
…pero hoy NO hablaremos (casi) de symfony/console
Hola,	holita		
deSymfony!	
ned	about	
Información	sobre	Ned	
ned	info	
Muestra	información	sobre	el	proyecto	actual				
ned	lint	[<files>]	
Comprueba	la	sintaxis	de	los	archivos	PHP,	XML,	JSON,	TWIG	
y	YAML	del	proyecto
$	ned
Distribución
Distribución
~/Projects/ned
index.php
ned
$	php	ned
ned.phar
Empaquetar	aplicaciones
#!/usr/bin/env php

<?php



Phar::mapPhar(‘ned.phar');


$autoload = require_once 'phar://ned.phar/vendor/autoload.php';



$app = new Application();

$app->run();

__HALT_COMPILER();
Empaquetar	aplicaciones{
"output": “bin/ned.phar",
"alias": "ned.phar",
"chmod": "0755",
"compactors": [
"HerreraBoxCompactorPhp"
],
"extract": false,
"files": ["LICENSE"],
"finder": [{
"name": ["*.php"],
"exclude": ["Tests", "tests"],
"in": ["vendor", "src"]
}],
"stub": "src/Resources/phar-stub.php",
“web": false
}
box-project/box2
$	box	build
box.json
Instalación
…quiero	descargar	la	
aplicación	correcta,	sin	
que	haya	sido	manipulada
…quiero	que	la	
aplicación	se	
comporte	de	forma	
naFva
…no	quiero	tener	que	
instalar	herramientas	
adicionales	para	
instalar	la	aplicación
ObjeFvos
HTTPS
Verificación	con	
funciones	hash
Experiencia		
de	usuario
Seguridad
Firma	digital
Ejecutable	sin		
.phar	en	$PATH
PHP	está	
disponible
PHAR
Instalación
$	sudo	mkdir	-p	/usr/local/bin	
$	sudo	curl	-LsS	hdps://symfony.com/installer	-o	/usr/local/bin/symfony	
$	sudo	chmod	a+x	/usr/local/bin/symfony
C:>	php	-r	"readfile('hdps://symfony.com/installer');"	>	symfony
$	php	-r	"copy('hdps://getcomposer.org/installer',	'composer-setup.php');"	
$	php	-r	"if	(hash_file('SHA384',	'composer-setup.php')	===	‘669656bab…’)	{	echo	
'Installer	verified';	}	else	{	echo	'Installer	corrupt';	unlink('composer-setup.php');	}	
echo	PHP_EOL;"	
$	php	composer-setup.php	
$	php	-r	"unlink('composer-setup.php');"
$	composer	global	require	"laravel/installer"
sin .phar
Instalación
Instalación
Seguridad
Experiencia
de usuario
Firma	digital
———————————
———————————
———————————
———————————
———————————
———————————
———————————
———————————
ned.phar.pubkeyned.phar
ned ned.pubkey
Firma	digital
$	ned
PHP	Fatal	error:		Uncaught	PharExcepFon:	phar	"/usr/local/bin/ned"	openssl	
signature	could	not	be	verified:	openssl	public	key	could	not	be	read	in	/usr/
local/bin/ned:9
Firma	digital
$	openssl	genrsa	-des3	-out	private.pem	4096
......................................++	
e	is	65537	(0x10001)

Enter	pass	phrase	for	private.pem:	*******	
Verifying	-	Enter	pass	phrase	for	private.pem:	*******
$	openssl	rsa	-in	private.pem	-out	private.pem
Enter	pass	phrase	for	private.pem:	*******	
wriFng	RSA	key
$	openssl	rsa	-in	private.pem	-pubout	>	ned.phar.pubkey
wriFng	RSA	key
Firma	digital
{
"output": “bin/ned.phar",
"alias": "ned.phar",
"chmod": "0755",
"compactors": [
"HerreraBoxCompactorPhp"
],
"extract": false,
"files": ["LICENSE"],
"finder": [{
"name": ["*.php"],
"exclude": ["Tests", "tests"],
"in": ["vendor", "src"]
}],
"stub": “src/Resources/phar-stub.php",
“key": "private.pem",
“web": false
}
box.json
$	phar	info	-f	`which	ned`
Hash-type:										OpenSSL	
Hash:															
6A3F4795D2D8BEEE8585CB0B76E7BFC7D539F00C5C0D889E900D24B9D54754213C8B8CD5DD38AB0B618C
7740C455BDBC7CA0B5B1AF62F3B5F9FFF908DA1E3F20AEE787D00586E3C3EDBC487935856C590A8554546
956D073CFDBF1C45306D8BB46C5D46642…	
Uncompressed-files:			1324	
Compressed-files:			0	
Compressed-gz:						0	
Compressed-bz2:					0	
Uncompressed-size:		3279431	
Compressed-size:				3279431	
Compression-raFo:		100%	
Stub-size:										438
$	phar	info	-f	`which	symfony`
Hash-type:										SHA-1	
Hash:															C4F3B517013B0DCEE595EEFD6700C6F746684191	
Uncompressed-files:	333	
Compressed-files:			333	
Compressed-gz:						333	
Compressed-bz2:					0	
Uncompressed-size:		707441	
Compressed-size:				207194	
Compression-raFo:		29.3%	
Stub-size:										232
"compression": “GZ"
"compression": “BZ2”
$	ned	self-update
self-update
	1.0	
ned
¿versión
nueva?
Sí
	1.0	
ned.bak
	1.1
ned
self-update
$updater = new Updater();
$updater->getStrategy()->setPharUrl(‘https://ned.io/ned.phar');
$updater->getStrategy()->setVersionUrl(‘https://ned.io/ned.version');
try {
if ($updater->update()) {
$output->writeln(‘Ned was successfully updated.');
} else {
$output->writeln(‘You are already using the latest version.');
}
return 0;
} catch (Exception $e) {
$output->writeln(‘Something happened…’);
return 1;
}
padraid/phar-updater
self-update
Deploy
#!/bin/bash
composer update --no-dev --optimize-autoloader
box build
shasum bin/ned.phar > bin/ned.version
scp bin/ned.{phar, phar.pubkey, version} ned-server:/var/www/html/
Portabilidad
Entornos	heterogéneos
$	ned
Parse	error:	syntax	error,	unexpected	'['	in	/usr/local/bin/ned	on	line	34
$	php	-v
PHP	5.3.27	(cli)	(built:	Oct		3	2013	05:18:10)	
Copyright	(c)	1997-2013	The	PHP	Group	
Zend	Engine	v2.3.0,	Copyright	(c)	1998-2013	Zend	Technologies
Versiones	de	PHP
$	ned
PHP	version	must	be	at	least	PHP	7.0.0.	Please,	upgrade.
Versión	mínima	stub	!=	Versión	mínima	aplicación<?php
…
__halt_compiler();
Consejos:	
1.	Stub	ejecutable	desde	PHP	5.3,	incluso	menos.
2.	Comprobación	de	versiones	en	el	stub.
Versiones	de	PHP
$	ned
PHP	Fatal	error:		Uncaught	Error:	Call	to	undefined	funcFon	Nedcurl_init()	in	
phar:///usr/local/bin/ned/src/Ned.php
$	php	-m	|	grep	curl	|	wc	-l
0
Extensiones	de	PHP
C:Usersraul>	ned
PHP	Fatal	error:		Uncaught	Error:	Call	to	undefined	funcFon	Nedposix_getuid()	in	
phar:///usr/local/bin/ned/src/Ned.php
Microso„	Windows	[Version	10.0.14393]	
©	2017	Microso„	CorporaFon.	All	rights	reserved.
Sistemas	operaFvos
Resumen
Minimiza	el	número	de	dependencias	de	“sistema”
Disponer	de	fallbacks	en	“PHP-land”
Comprueba	dependencias	obligatorias	en	el	stub
Reduce	las	dependencias	del	stub	a	0
CaracterísFcas	
profesionales
System	requirements	
[OK]	PHP	version	must	be	at	least	PHP	7.0.0	
[OK]	php-curl	extension	must	be	available	
[OK]	json_encode()	must	be	available	
[OK]	Memory	limit	should	be	at	least	128M	(using	256M)
$	ned	doctor
version_compare(
phpversion(), '7.0', ‘>=‘
)
extension_loaded(‘curl’)
function_exists(‘json_encode’)
ini_get(‘memory_limit’)
Requisitos	del	sistema
Requisitos	del	sistema
PHP
ini_get(‘bar’)
phpversion()
PHP_VERSION
PHP_MAJOR_VERSION
PHP_MINOR_VERSION
PHP_RELEASE_VERSION
PHP_VERSION_ID
PHP_EXTRA_VERSION
PHP_OS
PHP_OS_FAMILY
Lenguaje
function_exists(‘bar’)
method_exists(Foo::class, ‘bar’)
property_exists(Foo::class, ‘bar’)
class_exists(‘FooBar’)
interface_exists(‘FooInterface’)
trait_exists(‘FooTrait’)
extension_loaded(‘bar’)
phpversion(‘bar’)
Composer
vendor/composer/installed.json
7.2
do‰iles
private function isW
indows()

{

return defined('PHP_W
INDOW
S_VERSION_BUILD');

}
~	/	.nedconfig
private function getHomeDir()

{

if ($this->isWindows() && false !== ($home = getenv('USERPROFILE'))) {

return $home;

}



if (function_exists('posix_getuid') && function_exists('posix_getpwuid')) {

$info = posix_getpwuid(posix_getuid());



return $info['dir'];

}
do‰iles
XML YAML JSON INI TOML
Facilidad	de	lectura	para	un	humano Di#cil Muy	fácil Fácil Muy	fácil Muy	fácil
Facilidad	de	escritura	para	un	humano Di#cil Fácil Regular Muy	fácil Muy	fácil
Facilidad		para	tratarlo	desde	php

(Mapea	a	un	array	con	la	misma	estructura)
Di#cil Fácil Muy	fácil Fácil Fácil
Permite	comentarios Sí Sí No Sí Sí
MúlFples	niveles	de	anidación Sí Sí Sí No Sí
Archivos	de	configuración	escritos	por	humanos:
Archivos	de	configuración	internos:
INI	o	TOML
JSON
do‰iles
use SymfonyComponentConfigDefinitionConfigurationInterface;
use SymfonyComponentConfigDefinitionBuilderTreeBuilder;
 
class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
 
        return $treeBuilder->root('ned')

->children()

->booleanNode(‘show_colors’)

->defaultTrue()

->end();
    }
}
symfony/config
do‰ilesuse SymfonyComponentConfigFileLocator;
use SymfonyComponentConfigLoaderLoaderResolver;
use SymfonyComponentConfigDefinitionProcessor;
use NedConfigIniConfigLoader;
use NedConfigConfiguration;
 
$locator = new FileLocator($configDirs);
 
$loader = new IniConfigLoader($locator);
$configValues = $loader->load($locator->locate('config.ini'));
 
$processor = new Processor();
$configuration = new Configuration();
try {
    $processedConfiguration = $processor->processConfiguration(
        $configuration,
        $configValues
    );
 
    // configuración válida
} catch …
symfony/config
$resolver = new LoaderResolver([
    new IniConfigLoader($locator),
    new TomlConfigLoader($locator)
]);
$loader = new DelegatingLoader($resolver);
$cache = new ConfigCache($cachePath, true);
 if (!$cache->isFresh()) {
Array con la
configuración
de config.ini
do‰iles
Validar	la	configuración
Definir	valores	por	defecto
Soportar	varios	formatos	de	configuración
Cachear	configuración
symfony/config
$	cat	a.php	|	ned	lint
stdin
stdout
stderr
define(‘STDIN’, fopen(‘php://stdin’, ‘r’))
define(‘STDOUT’, fopen(‘php://stdout’, ‘w’))
define(‘STDERR’, fopen(‘php://stderr’, ‘w’))
stream_get_contents(STDIN)
Pipelines
$	ned	info	-	-format=cli
raul	in	~	[raulfraile/disFll	with	symfony]
name:	raulfraile/disFll	
framework:	symfony	
files:	15
ned status | grep name | awk -F
":" '{print $1}'
$
Personalización	del	prompt
$	ned	⇥⇥
about
lint
doctor
about
lint
doctor
Autocomplete
_ned()
{
local cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=( $(compgen -W "about info lint“ -- $cur) )
}
complete -F _ned ned
$	ned	get-compleFon-script
Autocomplete
bamarni/	
symfony-console-autocomplete
$	symfony-autocomplete	ned
^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	
^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	
^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	
^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C	^C
$	ned	lint
Checking	syntax	on	857.192.965	files.	This	might	take	a	while…
^C ^C ^C
$	ps	aux	|	grep	ned
raulfraile	90666		99.3		0.1		2519124		20212	s012		R+	4:08PM		0:02.35	php	/usr/local/bin/ned	lint
$	kill	90666
$	kill	-9	90666
SIGINT
SIGTERM
SIGKILL (No se puede interceptar…)
Señales
$signalHandler = function(int $signo, $signinfo) {

// cleanups

…
};



pcntl_signal(SIGTERM, $signalHandler); // kill

pcntl_signal(SIGINT, $signalHandler); // Ctrl-C
…
pcntl_signal_dispatch()
…
declare(ticks=1);
Señales
Comandos	del	sistema
$	ned	lint	--say
$process = new Process(
‘say “No syntax errors detected”’
);
$process->run();
SayMacosAdapter
VoiceAdapterInterface	
isSupported()	
speak(string	$message)
SayLinuxAdapter
EspeakAdapter
SpdSayAdapter
$process = new Process(‘command -v '.$command.' 2>&1');
$process->run();
if ($process->isSuccessful()) {
// command exists
}
Extensibilidad
$	ned	
composer:install	
composer:update	
composer:lint
finder:search	php
Extensibilidad
symfony:version	
symfony:install_crud
ned/composer
ned/core
ned/finder
symfony/ned-extension
Extensibilidad
ned/composer ned/finder symfony/ned-extension
ned/core ned/composer
ned/finder
symfony/	
ned-extension ned/core
autoload
Extensibilidad
cache/	
plugins/	
				composer.json	
				vendor/	
								ned/	
												composer/	
												finder/	
								symfony/	
												ned-extension/	
config.json
use ComposerConsoleApplication;
$currentDirectory = getcwd(); chdir($directory);
$app = new Application();
$output = new BufferedOutput();
$input = new ArrayInput([
'command' => 'install'
]);
$input->setInteractive(false);
$exitCode = $app->doRun(
$input,
$output
);
$output = $output->fetch();
chdir($currentDirectory);
Extensiones/Plugins	con	Composer
cache/	
plugins/	
config.json
~	/	.ned/
file_put_contents(
$homeDir . ’/.ned/plugins/composer.json’,
json_encode([
‘require’ => [
‘ned/composer’ => ‘~1.0’,
‘ned/finder’ => ‘~1.0’,
‘symfony/ned-extension’ => ‘~1.0’
]
])
)
$loader = require __DIR__ . ‘/vendor/autoload.php';
$pluginPsr4 = require $pluginsDir . ‘/vendor/composer/autoload_psr4.php';
foreach ($pluginPsr4 as $psr4Namespace => $psr4Path) {

$loader->addPsr4($psr4Namespace, $psr4Path);

}
cache/	
plugins/	
				composer.json	
				vendor/	
								composer/	
												installed.json	
								ned/	
												composer/	
												finder/	
								symfony/	
												ned-extension/	
config.json
Extensiones/Plugins	con	Composer
~	/	.ned/ // stub.php
…
$installed = json_decode(
$pluginsDir . ‘/vendor/composer/installed.json’,
true
);
foreach ($installed as $dependency) {
if ($dependency[‘type’] === ‘ned_plugin’) {
$commands = json_decode(
$pluginsDir . $dependency[‘name’] . ‘/plugin.json’,
true
);
…
}
}
$	ned	goodbye
• Archivos PHAR y distribución:

• Secure PHAR Automation - Matthew Weier O'phinney

https://mwop.net/blog/2015-12-14-secure-phar-
automation.html

• Dissecting the PHAR Format - Raúl Fraile

https://www.phparch.com/magazine/2015-2/october/ 

• Distributing PHARs: Pitfalls and Solutions - Pádraic
Brady

https://archive.is/hDwaA 

• Archivos de configuración / dotfiles:

• JSON as configuration files: please don’t - Martin
Tournoij

https://arp242.net/weblog/
json_as_configuration_files-_please_dont

• YAML: probably not so great after all - Martin Tournoij

https://arp242.net/weblog/
yaml_probably_not_so_great_after_all.html

• Symfony2 components overview: Config - Raúl Fraile

http://blog.servergrove.com/2014/02/21/symfony2-
components-overview-config/

• Autocomplete

• Writing your own Bash Completion Function - Fahd
Shariff 

http://fahdshariff.blogspot.com.es/2011/04/writing-
your-own-bash-completion.html

• Symfony console autocomplete

https://github.com/bamarni/symfony-console-
autocomplete 

• Software

• Box

https://box-project.github.io/box2/ 

• PHAR updater

https://github.com/humbug/phar-updater
Referencias

Aplicaciones CLI profesionales con Symfony