symfony without frameworkbundle€¦ · @tobiasnyholm tobias nyholm • full stack unicorn on...
TRANSCRIPT
@tobiasnyholm
Symfony without FrameworkBundle
Tobias Nyholm
https://joind.in/talk/f2ecf
@tobiasnyholm
Why?
@tobiasnyholm
Tobias Nyholm• Full stack unicorn on Happyr.com
• Certified Symfony developer
• Symfony core member
• Symfony CARE
• PHP-Stockholm
• Open source
@tobiasnyholm
Open source
PHP-cache
HTTPlug
Mailgun
LinkedIn API clientSwap
Stampie
BazingaGeocoderBundle
PHP-Geocoder
FriendsOfApi/boilerplateGuzzle Buzz
CacheBundlePSR7
SymfonyBundleTest
NSA
SimpleBus integrations
PSR HTTP clients
Neo4j
KNP Github API
PHP-Translation
Puli
Assert
Backup-manager/symfony
php-http/httplug-bundle php-http/multipart-stream
php-http/discovery
happyr/normal-distribution-bundle
nyholm/effective-interest-rate
MailgunBundle
league/geotools
NewRelicBundle
@tobiasnyholm
“Frameworks are slow”
@tobiasnyholm
areFrameworks slow
??? = Poor performance
@tobiasnyholm
What is a framework?
@tobiasnyholm
What is a framework?Symfony
@tobiasnyholm
svn checkout http://svn.symfony-project.com/tags/RELEASE_1_4_8 symfony
“Symfony folder”
@tobiasnyholm
composer create-project symfony/framework-standard-edition acme "2.8.*"
“vendor/symfony/symfony”
@tobiasnyholm
composer create-project symfony/framework-standard-edition acme “3.4.*”
“vendor/symfony/symfony”
@tobiasnyholm
composer create-project symfony/skeleton acme
“vendor/symfony/*”
@tobiasnyholm
{ "name": "symfony/skeleton", "require": { "php": "^7.1.3", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "*", "symfony/flex": "^1.1", "symfony/framework-bundle": "*", "symfony/yaml": "*" }, "require-dev": { "symfony/dotenv": "*" } }
@tobiasnyholm
What is a framework?Symfony
@tobiasnyholm
{ "name": "symfony/framework-bundle", "type": "symfony-bundle", "description": "Symfony FrameworkBundle", "require": { "php": "^7.1.3", "ext-xml": "*", "symfony/cache": "~3.4|~4.0", "symfony/dependency-injection": "^4.2", "symfony/config": "~4.2", "symfony/event-dispatcher": "^4.1", "symfony/http-foundation": "^4.1.2", "symfony/http-kernel": "^4.2", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", "symfony/routing": "^4.1" } }
@tobiasnyholm
Level of Magic
0 %10 %20 %30 %40 %50 %60 %70 %80 %90 %
100 %
Symfony version
1 2 3 4 5
Awesome
User responsibillity
Magic
@tobiasnyholm
an ecosystemwhatever we makeSymfony is a community
(pause for tweeting)
#Symfony_Live
@tobiasnyholm
areFrameworks slow
Our code = Poor performance
@tobiasnyholm
Let’s talk about performance
@tobiasnyholm
Rule 1: Buy a better server
@tobiasnyholm
Rule 2: Use Varnish
@tobiasnyholm
Req
uest
s p
er t
ime
unit
0
1000
2000
3000
4000
No Varnish With Varnish
@tobiasnyholm
Rule 3: Run less code
@tobiasnyholm
section .text global _start ; must be declared for linker (ld)
_start: ; tell linker entry point
mov edx,len ; message length mov ecx,msg ; message to write mov ebx,1 ; file descriptor (stdout) mov eax,4 ; system call number (sys_write) int 0x80 ; call kernel
mov eax,1 ; system call number (sys_exit) int 0x80 ; call kernel
section .data
msg db 'Hello, world!',0xa ; our dear string len equ $ - msg ; length of our dear string
@tobiasnyholm
More code = More “ticks”
@tobiasnyholm
Version Lines of codeLines
executedFunction calls Memory Time
Symfony 1.4 172 000 2 401 10 588 1 200 Kb 198 ms
Symfony 2.8 415 700 1 928 5 051 1 600 Kb 100 ms
Symfony 3.4 433 600 2 649 4 004 1 600 Kb 82 ms
Symfony 4.1 136 000 1 029 1 421 500 Kb 31 ms
Hello world
@tobiasnyholm
Fastest application
<?php // index.php
if (isset($_GET['page']) && $_GET['page'] === 'foo') { echo "Foo page <br>"; } else { echo "Welcome to index! <br>"; }
@tobiasnyholm
What is your application doing?
@tobiasnyholm
<?php
$uri = $_SERVER['REQUEST_URI']; $base = 'https://happyr.com';
if (substr($uri, 0, 1) !== 'j') { redirect($base); }
redirect(sprintf('%s/x/job/%d-x_x', $base, substr($uri, 1)));
function redirect($url) { header('Location: '.$url); exit(0); }
@tobiasnyholm
Complexity
Redirect symfony/skeleton symfony/website-skeleton
Your application?
@tobiasnyholm
Do you need this?
@tobiasnyholm
Do you need this?
Forms
Security
Validation
Router
Dependency injectionEvent dispatcher
Serializer
Yaml Translation
HTTP KernelHTTP Foundation
FinderDebug
@tobiasnyholm
Do we need HTTP?
@tobiasnyholm
HTTP Foundation
$_SERVER[‘REQUEST_URI’]
vs
@tobiasnyholm
Messenger component?
@tobiasnyholm
Doctrine or just PDO?
@tobiasnyholm
Building a small application
@tobiasnyholm
Workflow component
<?php
use Symfony\Component\Workflow\DefinitionBuilder; use Symfony\Component\Workflow\Transition; use Symfony\Component\Workflow\Workflow; use Symfony\Component\Workflow\MarkingStore\SingleStateMarkingStore;
$definitionBuilder = new DefinitionBuilder(); $definition = $definitionBuilder->addPlaces(['draft', 'review', 'rejected', 'published']) ->addTransition(new Transition('to_review', 'draft', 'review')) ->addTransition(new Transition('publish', 'review', 'published')) ->addTransition(new Transition('reject', 'review', 'rejected')) ->build() ;
$marking = new SingleStateMarkingStore('currentState'); $workflow = new Workflow($definition, $marking);
$article = /* fetch from database */ if ($workflow->can($article, 'publish')) { $workflow->apply($article, 'publish');
// TODO redirect or any other action ... }
@tobiasnyholm
Cache component
namespace Symfony\Contracts\Cache;
/** * Gets/Stores items from/to a cache. * * On cache misses, a callback is called that should return the missing value. * This callback is given an ItemInterface object corresponding to the needed key, * that could be used e.g. for expiration control. */ interface CacheInterface { /** * @param callable(ItemInterface) $callback Should return the computed value for the given * key/item * @param float|null $beta A float that, as it grows, controls the likeliness * of triggering early expiration. 0 disables it, * INF forces immediate expiration. * * @return mixed The value corresponding to the provided key * * @throws InvalidArgumentException When $key is not valid or when $beta is negative */ public function get(string $key, callable $callback, float $beta = null); }
$result = $cache->get('foobar', function (ItemInterface $item) { $pi = null; /* Calculation of pi */ $item->expiresAfter(3600);
return $pi; });
$result = $cache->get('foobar', function (ItemInterface $item) { $pi = null; /* Calculation of pi */ $item->expiresAfter(3600);
return $pi; });
Time
T: 0 T: 3600
@tobiasnyholm
Middleware pattern
<?php
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use App\Runner;
require __DIR__.'/../vendor/autoload.php';
$request = Request::createFromGlobals(); $response = new Response();
$middleware[] = function(Request $request, Response $response, callable $next) { if ($request->getMethod() !== 'GET') { return new Response('Method not allowed', 405); } return $next($request, $response); };
$middleware[] = function(Request $request, Response $response, callable $next) { $response->setContent('foobar'); return $next($response, $response); };
$runner = new Runner($middleware); $response = $runner($request, $response);
// Send response echo $response->getBody();
namespace App;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request;
class Runner { /** @var callable[] */ private $queue;
public function __construct(array $queue) { $this->queue = $queue; }
public function __invoke(Request $request, Response $response) { $middleware = array_shift($this->queue); if (null === $middleware) { // Default return function (Request $request, Response $response, callable $next) { return $response; }; }
return $middleware($request, $response, $this); } }
<?php
namespace App\Middleware;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request;
interface MiddlewareInterface { public function __invoke(Request $request, Response $response, callable $next); }
namespace App\Middleware;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request;
class Router implements MiddlewareInterface { public function __invoke(Request $request, Response $response, callable $next) { $uri = $request->getUri();
switch ($uri) { case '/': $response = (new \App\Controller\StartpageController())->run($request); break; case '/foo': $response = (new \App\Controller\FooController())->run($request); break; default: $response->setStatusCode(404); $response->setContent('Not Found'); break; }
return $next($request, $response); } }
<?php
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use App\Runner;
require __DIR__.'/../vendor/autoload.php';
$request = Request::createFromGlobals(); $response = new Response();
$middleware[] = new Cache; $middleware[] = new NewRelic; $middleware[] = new Security; $middleware[] = new Router;
$runner = new Runner($middleware); $response = $runner($request, $response);
// Send response echo $response->getBody();
@tobiasnyholm
Custom kernel
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class Kernel { private $booted = false; private $debug; private $env;
/** @var Container */ private $container;
public function __construct(string $env, bool $debug = false) { $this->debug = $debug; $this->env = $env; }
/** * Handle a Request and turn it in to a response. */ public function handle(Request $request): Response { $this->boot();
require_once $containerDumpFile; $container = new \CachedContainer(); } else { $container = new ContainerBuilder(); $container->setParameter('kernel.project_dir', $this->getProjectDir()); $container->setParameter('kernel.environment', $this->env);
$container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('kernel.event_subscriber');
$container->registerForAutoconfiguration(Command::class) ->addTag('console.command');
$container->addCompilerPass(new AddConsoleCommandPass()); $container->addCompilerPass( new RegisterListenersPass(EventDispatcherInterface::class), PassConfig::TYPE_BEFORE_REMOVING );
$fileLocator = new FileLocator($this->getProjectDir().'/config'); $loader = new YamlFileLoader($container, $fileLocator); try { $loader->load('services.yaml'); $loader->load('services_'.$this->env.'.yaml'); } catch (FileLocatorFileNotFoundException $e) { }
$container->compile();
//dump the container @mkdir(dirname($containerDumpFile), 0777, true);
@tobiasnyholm
Too much boilerplate?Use the FrameworkBundle
use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel { use MicroKernelTrait;
public function registerBundles() { return [ new FrameworkBundle(), ]; }
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) { $confDir = $this->getProjectDir().'/config'; $loader->load($confDir.'/services.yaml'); $loader->load($confDir.'/services_'.$this->environment.'.yaml'); }
protected function configureRoutes(RouteCollectionBuilder $routes) { $confDir = $this->getProjectDir().'/config'; $routes->import($confDir.'/routes.yaml');
use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel { use MicroKernelTrait;
public function registerBundles() { return [ new FrameworkBundle(), ]; }
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) { $confDir = $this->getProjectDir().'/config'; $loader->load($confDir.'/services.yaml'); $loader->load($confDir.'/services_'.$this->environment.'.yaml'); }
protected function configureRoutes(RouteCollectionBuilder $routes) { $confDir = $this->getProjectDir().'/config'; $routes->import($confDir.'/routes.yaml');
/** * A Kernel that provides configuration hooks. * * @author Ryan Weaver <[email protected]> * @author Fabien Potencier <[email protected]> */ trait MicroKernelTrait { abstract protected function configureRoutes(RouteCollectionBuilder $routes); abstract protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader);
public function registerContainerConfiguration(LoaderInterface $loader) { $loader->load(function (ContainerBuilder $container) use ($loader) { $container->loadFromExtension('framework', array( 'router' => array( 'resource' => 'kernel::loadRoutes', 'type' => 'service', ), ));
if ($this instanceof EventSubscriberInterface) { $container->register('kernel', static::class) ->setSynthetic(true) ->setPublic(true) ->addTag('kernel.event_subscriber') ; }
@tobiasnyholm
Improve performance
@tobiasnyholm
Improve performance
#1: Buy a better server
#2: Use Varnish
#3: Run less code
@tobiasnyholm
Improve performance
#1: Buy a better computer
#2: Use cache
#3: Run less code
@tobiasnyholm
Profile your application
@tobiasnyholm
“Normal” PHP-FPM
App
App
App
App
HTTP FastCGI
@tobiasnyholm
PHPFastCGI
App
App
App
App
HTTP FastC
GI
https://github.com/PHPFastCGI/FastCGIDaemon
<?php
require_once dirname(__FILE__) . '/../vendor/autoload.php';
use PHPFastCGI\FastCGIDaemon\ApplicationFactory; use PHPFastCGI\FastCGIDaemon\Http\RequestInterface; use Symfony\Component\HttpFoundation\Response;
$kernel = function (RequestInterface $request) { $sfRequest = $request->getHttpFoundationRequest();
return new Response('<h1>Hello, World!</h1>' . $sfRequest->getUri()); };
$application = (new ApplicationFactory)->createApplication($kernel); $application->run();
https://github.com/PHPFastCGI/FastCGIDaemon
<?php
require_once dirname(__FILE__) . '/../vendor/autoload.php';
use PHPFastCGI\FastCGIDaemon\ApplicationFactory; use PHPFastCGI\FastCGIDaemon\Http\RequestInterface; use Symfony\Component\HttpFoundation\Response;
$kernel = function (RequestInterface $request) { $sfRequest = $request->getHttpFoundationRequest();
$response = new Response();
$middleware[] = new Cache; $middleware[] = new NewRelic; $middleware[] = new Security; $middleware[] = new Router;
$runner = new Runner($middleware); $response = $runner($sfRequest, $response);
return $response; };
$application = (new ApplicationFactory)->createApplication($kernel); $application->run();
use PHPFastCGI\FastCGIDaemon\Http\RequestInterface; use PHPFastCGI\FastCGIDaemon\KernelInterface as PHPFastCGIKernel; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\TerminableInterface;
class FastCGIKernel implements PHPFastCGIKernel { private $kernel;
public function __construct(KernelInterface $kernel) { $this->kernel = $kernel; }
public function handleRequest(RequestInterface $request) { $this->kernel->boot();
$symfonyRequest = $request->getHttpFoundationRequest(); $symfonyResponse = $this->kernel->handle($symfonyRequest);
if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($symfonyRequest, $symfonyResponse); }
return $symfonyResponse; } }
@tobiasnyholm
< 10ms
@tobiasnyholm
< 5ms
@tobiasnyholm
@tobiasnyholm
Remember
@tobiasnyholm
– Tobias Nyholm
“We are Symfony”
https://joind.in/talk/f2ecf