Vich Upload Bundle Crete File From String
How to create a Symfony 5 bundle
Symfony Bundles
A bundle tin be considered essentially a plugin for a Symfony projection; it tin wrap whatsoever functionality constitute in a Symfony application including configuration, controllers, routes, services, upshot listeners, templates, etc.
Until recent versions of Symfony (v4 I think off the peak of my head?) the recommended style to build your application was to encapsulate it inside a bundle. We no longer do this, but bundles are still useful for mutual functionality you'd like to share beyond multiple projects.
In this tutorial, nosotros're going to create a bundle for Symfony v which allows someone building an application to easily configure some mutual HTTP headers relating to security. Once the user has installed our bundle, they need only set a few options in a uncomplicated YAML file and the relevant headers will exist added automatically to all their application's responses.
To follow this tutorial, y'all will need PHP (at least v7.4), Composer and of course a Symfony app to test your package against. This tutorial assumes prior working knowledge of PHP, Composer and Symfony.
The headers nosotros'll be adding via our package are Content-Security-Policy
, X-Frame-Options
, Strict-Transport-Security
and X-Content-Blazon-Options
.
Setting up a new bundle
Older versions of Symfony had a command line helper to generate a bundle skeleton. Sadly this is no longer around, so we're going to create a packet from scratch.
Create a new project directory SecurityHeadersBundle
to work in and within that directory, create the following empty files and folder structure:
SecurityHeadersBundle/ ├─ src/ │ ├─ SecurityHeadersBundle.php │ ├─ DependencyInjection/ │ ├─ EventSubscriber/ │ ├─ Resource/ │ │ ├─ config/ │ │ │ ├─ services.yaml ├─ composer.json
This is our bundle skeleton.
Open up upward composer.json
in your favourite IDE/editor and add together the following, obviously replacing my details with yours:
{ "name": "dwgebler/security-headers-bundle", "description": "Set security headers for Symfony apps through middleware", "blazon": "symfony-packet", "require": { "php": ">=7.4", "symfony/framework-bundle": "^5.2" }, "autoload": { "psr-4": { "Gebler\\SecurityHeadersBundle\\": "src/" } }, "license": "MIT", "authors": [ { "name": "David Gebler", "e-mail": "me@davegebler.com" } ] }
Now head on over to your concluding/command line and run composer install
from the project directory. This will install the dependencies and create the vendor/
directory within your project directory.
Open up SecurityHeadersBundle.php
and add the following code:
<?php namespace Gebler\SecurityHeadersBundle; use Symfony\Component\HttpKernel\Bundle\Package; /** * SecurityHeadersBundle */ form SecurityHeadersBundle extends Packet { }
Congratulations! You lot've at present created a bundle which tin be installed and activated in a Symfony project. That was piece of cake!
If yous have an existing Symfony project (just create one from the skeleton if nothing else), add together a reference to your new bundle source in composer.json
like then:
"repositories": [ {"type": "path", "url": "/path/to/SecurityHeadersBundle"} ], ... "require": { ... "dwgebler/security-headers-packet": "dev-master", }
And and so in your Symfony app'southward config/bundles.php
, add the post-obit line to the bundles array:
Gebler\SecurityHeadersBundle\SecurityHeadersBundle::class => ['all' => truthful],
Make our bundle do something
Our bundle works and can be added to a Symfony app, but information technology's functionally empty. Permit'south add an event subscriber to listen to the kernel.response event and add a header to the response.
We'll need to create a few files to do this - and I must stress naming is important here. Symfony does a fiddling bit of magic in respect of loading bundles and configuration of services, so these file names are not suggestions, they are mandatory.
Within src/EventSubscriber
directory, create a new file called ResponseSubscriber.php
with the following content. This is our event subscriber (non quite the aforementioned as an event listener in Symfony terminology).
<?php namespace Gebler\SecurityHeadersBundle\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class ResponseSubscriber implements EventSubscriberInterface { public static role getSubscribedEvents() { render [ KernelEvents::RESPONSE => [ ['addSecurityHeaders', 0], ], ]; } public role addSecurityHeaders(ResponseEvent $result) { $response = $event->getResponse(); $response->headers->set up('X-Header-Prepare-Past', 'My custom bundle'); } }
Great! This subscriber listens to the kernel.response
effect, which is dispatched simply before a response is sent, and sets a header X-Header-Set-Past
and then nosotros can check in the browser and ensure our bundle is working.
Simply we're non done yet. If nosotros re-run our application, Symfony won't know anything about this class or that it's an event subscriber. We nevertheless need to create a couple more files.
In src/Resources/config
, open up the empty services.yaml
file y'all created earlier and drop in the following:
services: gebler_security_headers.response_subscriber: class: Gebler\SecurityHeadersBundle\EventSubscriber\ResponseSubscriber tags: - { name: kernel.event_subscriber }
At present in src/DependencyInjection/
create a file called SecurityHeadersExtension.php
and add the following:
<?php namespace Gebler\SecurityHeadersBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; employ Symfony\Component\DependencyInjection\Extension\Extension; apply Symfony\Component\DependencyInjection\Loader\YamlFileLoader; /** * SecurityHeadersExtension */ class SecurityHeadersExtension extends Extension { public office load(array $configs, ContainerBuilder $container) { $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yaml'); } }
This file tells Symfony to load upward and parse our services.yaml
when our parcel is initialized, which in turn registers our ResponseSubscriber
service grade and allows Symfony to configure information technology every bit an event subscriber.
With these files now in place, in your sample Symfony app, yous should be able to run something like composer update dwgebler/security-headers-bundle:dev-master
to sync up the new bundle source code and check the headers from a request in the browser, or using a tool like Postman:
Awesome! Nosotros've created a parcel, installed it on an app (an old local copy of my blog in this example) and it's successfully calculation a header to all the page responses!
Now nosotros've made our package functional, the next step is to make information technology useful.
Make our bundle configurable
It's absurd that nosotros're adding a custom header to responses using our bundle, simply now we want to supercede X-Header-Prepare-By
with some of those security headers we talked about.
To commencement with, we could merely replace the line $response->headers->set('X-Header-Set-By', 'My custom packet')
with some other lines like:
$response->headers->gear up('X-Frame-Options`', 'deny'); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('Strict-Transport-Security', 'max-historic period=63072000; includeSubDomains; preload'); $response->headers->prepare('Content-Security-Policy', 'script-src \'self\'');
And this would be fine and bully in terms of getting the headers set up, merely what we actually want is for users of our bundle to be able to configure the values for these headers and choose which headers to include in some prissy, easy way.
The best way would exist for them to be able to modify a security_headers.yaml
file in their normal Symfony packages config directory. We also don't desire them to have to faff around setting the exact header value strings - some similar X-Frame-Options
may be simple enough, but others similar Content-Security-Policy
are a nightmare to go right.
For example, a CSP header of script-src 'self'
is pretty useless for my weblog; in guild to part correctly my CSP would have to look more similar this:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://kit.fontawesome.com; style-src 'self' https://fonts.googleapis.com; manner-src-attr 'dangerous-inline'; img-src *; font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com; connect-src 'self'; frame-src 'self'; course-activity 'self'; upgrade-insecure-requests; block-all-mixed-content
What nosotros're going to practice is allow our user to configure these kind of values in a more intuitive way, using YAML. Then let'due south aid them do that in our bundle.
Package configuration in the Symfony app
If we were creating a bundle and publishing it, we'd create a Flex recipe to re-create the skeleton configuration file to the application's config/packages
directory. For now, in your Symfony app, merely create a new file in that directory chosen security_headers.yaml
. Information technology should look similar this:
security_headers: frames: sameorigin sniff_mimes: fake https: required: truthful subdomains: truthful preload: true content: default: self scripts: - cocky - https://world wide web.googletagmanager.com - https://kit.fontawesome.com styles: - cocky - https://fonts.googleapis.com styles_inline: truthful upgrade_insecure: truthful
This is not comprehensive by any means, but it'southward easy to aggrandize on in your own bundles which need to read config once you understand how information technology all works.
At present, in the src/DependencyInjection/
directory, create a new file called Configuration.php
and add the post-obit code:
<?php /** * Configuration course. */ namespace Gebler\SecurityHeadersBundle\DependencyInjection; use Symfony\Component\Config\Definition\Architect\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; /** * Configuration */ class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder() { $treeBuilder = new TreeBuilder('security_headers'); $treeBuilder->getRootNode() ->children() ->scalarNode('frames')->end() ->scalarNode('sniff_mimes')->end() ->arrayNode('https')->children() ->booleanNode('required')->end() ->booleanNode('subdomains')->end() ->booleanNode('preload')->terminate() ->end() ->finish() ->arrayNode('content')->children() ->scalarNode('default')->end() ->scalarNode('upgrade_insecure')->end() ->scalarNode('styles_inline')->cease() ->arrayNode('scripts')->scalarPrototype()->end()->end() ->arrayNode('styles')->scalarPrototype()->end()->end() ->end() ->end(); return $treeBuilder; } }
You can see we're defining the structure of our config; we also accept the config from the awarding's YAML file available every bit a normalised array in the SecurityHeadersExtension
class, but defining it this way allows Symfony to manage merging the config when different values are provided for different environments, dev, prod etc. likewise as giving us a sensible mode to prepare defaults and optional parameters (though we oasis't done that here).
Symfony configuration reference contains more than details about how to configure the tree builder.
Load config from the configuration file
Now we just demand to load the merged configuration inside our bundle. Open up src/DependencyInjection/SecurityHeadersExtension.php
again and edit the load
function and then it looks like the post-obit:
public function load(array $configs, ContainerBuilder $container) { $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yaml'); $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); foreach ($config as $primal => $value) { $container->setParameter('security_headers.' . $key, $value); } }
This will let our upshot subscriber to grab the parameters from the user's security_headers.yaml
.
Update our upshot subscriber
The last piece is to read these parameters within our event subscriber and apply them to construct the response headers we desire to send. There are a few means of doing this; for simple service parameters (bools, scalars and the like) y'all would usually inject them in to the constructor of your service and reference the parameters within your services.yaml
, similar you do with parameters in a Symfony awarding.
Considering we have quite a complex parameter set, we're going to grab from the container via a parameter service, which is what we'll inject in to our event subscriber. Worth pointing out this is non actually a best practice, only it'll do for the sake of case, particularly as information technology shows you how to manually wire in other service classes your bundles may use as dependencies.
Caput dorsum to src/EventSubscriber/ResponseSubscriber.php
and add a constructor to the grade:
public function __construct(ParameterBagInterface $parameterBag) { $this->parameterBag = $parameterBag; }
Y'all'll too want to add the parameterBag
property to the class and import the interface from its namespace. Your file should end up looking like this:
<?php namespace Gebler\SecurityHeadersBundle\EventSubscriber; apply Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; employ Symfony\Component\HttpKernel\Event\ResponseEvent; employ Symfony\Component\HttpKernel\KernelEvents; class ResponseSubscriber implements EventSubscriberInterface { private ParameterBagInterface $parameterBag; public function __construct(ParameterBagInterface $parameterBag) { $this->parameterBag = $parameterBag; } public static office getSubscribedEvents() { return [ KernelEvents::RESPONSE => [ ['addSecurityHeaders', 0], ], ]; } public function addSecurityHeaders(ResponseEvent $consequence) { $response = $consequence->getResponse(); $response->headers->set('Ten-Header-Prepare-By', 'My super custom security bundle'); } }
In our src/Resources/config/services.yaml
, nosotros also need to make an update:
services: gebler_security_headers.response_subscriber: class: Gebler\SecurityHeadersBundle\EventSubscriber\ResponseSubscriber arguments: $parameterBag: "@parameter_bag" tags: - { name: kernel.event_subscriber }
Now the moment we've been waiting for...we're set up to read these parameters in the issue subscriber and construct some headers!
Edit the addSecurityHeaders()
function; take out the X-Header-Set-By
line and supercede with the following:
public role addSecurityHeaders(ResponseEvent $event) { $frameOptions = $this->parameterBag->get('security_headers.frames'); $mimeSniffing = $this->parameterBag->become('security_headers.sniff_mimes'); $https = $this->parameterBag->become('security_headers.https'); $csp = $this->parameterBag->become('security_headers.content'); $strictTransport = ''; $contentSecurityPolicy = "default-src '{$csp['default']}'"; if ($https['required']) { $strictTransport = 'max-age=63072000'; if ($https['subdomains']) { $strictTransport .= '; includeSubDomains'; } if ($https['preload']) { $strictTransport .= '; preload'; } } if ($csp['upgrade_insecure']) { $contentSecurityPolicy .= "; upgrade-insecure-requests"; } if ($csp['styles_inline']) { $contentSecurityPolicy .= "; fashion-src-attr 'unsafe-inline'"; } $contentSecurityPolicy .= "; script-src"; foreach ($csp['scripts'] equally $src) { if ($src === "self") { $src = "'self'"; } $contentSecurityPolicy .= " ".$src; } $contentSecurityPolicy .= "; style-src"; foreach ($csp['styles'] as $src) { if ($src === "self") { $src = "'self'"; } $contentSecurityPolicy .= " ".$src; } $response = $event->getResponse(); if ($mimeSniffing === false) { $response->headers->set('X-Content-Type-Options', 'nosniff'); } $response->headers->set('X-Frame-Options`', $frameOptions); $response->headers->gear up('Strict-Transport-Security', $strictTransport); $response->headers->fix('Content-Security-Policy', $contentSecurityPolicy); }
That's information technology! If you're running dev-master
of your bundle inside a sample Symfony app, update the bundle installation and check the headers coming through in your browser:
Improving the bundle
Hopefully this tutorial has given you a solid start in how to create a reusable Symfony 5 bundle from scratch. In that location are of class a few things left to exercise before we accept a bundle we could distribute in the wild:
- There are many more
Content-Security-Policy
options and permutations to deal with in our event subscriber. - Nosotros didn't set whatsoever defaults or deal with missing/optional configuration for the bundle.
- Nosotros oasis't written any tests.
- We injected a parameter purse rather than the arguments specific to the packet.
- We'd need to write a Flex recipe so Symfony users tin can install our bundle in their apps easily with Composer.
You tin download the source code for our demo bundle on my GitHub.
Until side by side time đŸ˜€️
Comments
All comments are pre-chastened and will not exist published until approval.
Source: https://davegebler.com/post/php/how-to-create-a-symfony-5-bundle
0 Response to "Vich Upload Bundle Crete File From String"
Enregistrer un commentaire