├── .gitignore ├── README.md ├── Vagrantfile ├── bin ├── .gitignore └── console ├── composer.json ├── composer.lock ├── config ├── dev.php └── prod.php ├── resources ├── hosts ├── httpd.conf ├── mapping │ └── serializer │ │ ├── CQRSBlog.BlogEngine.DomainModel.CommentId.yml │ │ ├── CQRSBlog.BlogEngine.DomainModel.CommentWasAdded.yml │ │ ├── CQRSBlog.BlogEngine.DomainModel.PostContentWasChanged.yml │ │ ├── CQRSBlog.BlogEngine.DomainModel.PostId.yml │ │ ├── CQRSBlog.BlogEngine.DomainModel.PostTitleWasChanged.yml │ │ ├── CQRSBlog.BlogEngine.DomainModel.PostWasCreated.yml │ │ └── CQRSBlog.BlogEngine.DomainModel.PostWasPublished.yml └── provisioning.sh ├── src ├── CQRSBlog │ ├── BlogEngine │ │ ├── Command │ │ │ ├── CommentCommand.php │ │ │ ├── CommentHandler.php │ │ │ ├── CreatePostCommand.php │ │ │ ├── CreatePostHandler.php │ │ │ ├── PublishPostCommand.php │ │ │ ├── PublishPostHandler.php │ │ │ ├── UpdatePostCommand.php │ │ │ └── UpdatePostHandler.php │ │ ├── DomainModel │ │ │ ├── Comment.php │ │ │ ├── CommentId.php │ │ │ ├── CommentWasAdded.php │ │ │ ├── Post.php │ │ │ ├── PostContentWasChanged.php │ │ │ ├── PostId.php │ │ │ ├── PostProjection.php │ │ │ ├── PostRepository.php │ │ │ ├── PostTitleWasChanged.php │ │ │ ├── PostView.php │ │ │ ├── PostViewRepository.php │ │ │ ├── PostWasCreated.php │ │ │ └── PostWasPublished.php │ │ ├── Infrastructure │ │ │ ├── Persistence │ │ │ │ ├── EventStore │ │ │ │ │ ├── EventStore.php │ │ │ │ │ ├── MongoDbEventStore.php │ │ │ │ │ ├── PDOEventStore.php │ │ │ │ │ ├── PostRepository.php │ │ │ │ │ └── RedisEventStore.php │ │ │ │ ├── MongoDb │ │ │ │ │ └── PostViewRepository.php │ │ │ │ ├── PDO │ │ │ │ │ └── PostViewRepository.php │ │ │ │ └── Redis │ │ │ │ │ └── PostViewRepository.php │ │ │ └── Projection │ │ │ │ ├── BaseProjection.php │ │ │ │ ├── MongoDb │ │ │ │ └── PostProjection.php │ │ │ │ ├── PDO │ │ │ │ └── PostProjection.php │ │ │ │ └── Redis │ │ │ │ └── PostProjection.php │ │ └── Query │ │ │ ├── AllPostsQuery.php │ │ │ ├── AllPostsQueryHandler.php │ │ │ ├── PostQuery.php │ │ │ └── PostQueryHandler.php │ └── Common │ │ ├── DomainModel │ │ └── Projection.php │ │ └── ServiceBus │ │ ├── CommandBus.php │ │ ├── HandlerNotFoundException.php │ │ └── QueryBus.php ├── app.php ├── console.php └── controllers.php ├── templates ├── bootstrap.html.twig ├── errors │ ├── 404.html.twig │ ├── 4xx.html.twig │ ├── 500.html.twig │ ├── 5xx.html.twig │ └── default.html.twig ├── index.html.twig ├── layout.html.twig ├── new_post.html.twig └── post.html.twig ├── var ├── cache │ └── .gitignore └── logs │ └── .gitignore └── web ├── assets ├── css │ ├── blog.css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ └── bootstrap.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── jquery.min.js ├── index.php └── index_dev.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.vagrant -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CQRS + ES Blog Engine 2 | ===================== 3 | 4 | A blog engine written in PHP and powered with CQRS + Event Sourcing 5 | 6 | ## Requirements 7 | 8 | All you need to run this application is Vagrant. 9 | 10 | **http://www.vagrantup.com/** 11 | 12 | ## Installation 13 | 14 | ```bash 15 | git clone https://github.com/theUniC/blog-cqrs.git 16 | cd blog-cqrs 17 | wget http://getcomposer.org/composer.phar 18 | php composer.phar install 19 | ``` 20 | 21 | Next you have to update your hosts file (usually located at */etc/hosts*), with the line below 22 | 23 | 172.21.99.6 mydddblog.test www.mydddblog.test redis.mydddblog.test 24 | 25 | ## Running the application 26 | 27 | From the root application folder, run 28 | 29 | vagrant up --provision 30 | 31 | When vagrant finishes bootstraping the VM, open up a browser and go to 32 | 33 | **http://www.mydddblog.test** 34 | 35 | Have fun! 36 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | config.vm.box = "puphpet/debian75-x64" 8 | config.vm.hostname = "webserver" 9 | config.vm.network :private_network, ip: "172.21.99.6" 10 | 11 | config.vm.synced_folder ".", "/var/www/cqrs-blog-engine", type: "nfs" 12 | 13 | config.vm.provider :virtualbox do |vb| 14 | vb.name = "CQRS Blog Engine" 15 | vb.customize ["modifyvm", :id, "--memory", "1024"] 16 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 17 | vb.customize ["modifyvm", :id, "--hpet", "on"] 18 | end 19 | 20 | config.vm.provision "shell", path: "resources/provisioning.sh" 21 | end -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !console 3 | !.gitignore -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); 12 | 13 | $app = require __DIR__.'/../src/app.php'; 14 | require __DIR__.'/../config/'.$env.'.php'; 15 | $console = require __DIR__.'/../src/console.php'; 16 | $console->run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "christian.soronellas/blog-cqrs-es", 3 | "require": { 4 | "buttercup/protects": "dev-master", 5 | "rhumsaa/uuid": "~2.7", 6 | "moontoast/math": "*", 7 | "mathiasverraes/classfunctions": "1.*", 8 | "silex/silex": "~1.2", 9 | "silex/web-profiler": "~1.0", 10 | "symfony/browser-kit": "~2.4", 11 | "symfony/class-loader": "~2.4", 12 | "symfony/config": "~2.4", 13 | "symfony/console": "~2.4", 14 | "symfony/form": "~2.4", 15 | "symfony/css-selector": "~2.4", 16 | "symfony/debug": "~2.4", 17 | "symfony/finder": "~2.4", 18 | "symfony/monolog-bridge": "~2.4", 19 | "symfony/process": "~2.4", 20 | "symfony/security": "~2.4", 21 | "symfony/translation": "~2.4", 22 | "symfony/twig-bridge": "~2.4", 23 | "symfony/validator": "~2.4", 24 | "symfony/serializer": "~2.4", 25 | "jms/serializer": "0.*", 26 | "predis/predis": "~0.8", 27 | "predis/service-provider": "~0.4", 28 | "braincrafted/bootstrap-bundle": "dev-master", 29 | "csanquer/pdo-service-provider": "v0.4.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "~4.1", 33 | "mockery/mockery": "0.9.0" 34 | }, 35 | "authors": [ 36 | { 37 | "name": "Christian Soronellas", 38 | "email": "christian.soronellas@atrapalo.com" 39 | } 40 | ], 41 | "minimum-stability": "stable", 42 | "autoload": { 43 | "psr-0": { 44 | "": ["src/"] 45 | } 46 | }, 47 | "config": { 48 | "bin-dir": "bin/" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/dev.php: -------------------------------------------------------------------------------- 1 | register(new MonologServiceProvider(), array( 13 | 'monolog.logfile' => __DIR__.'/../var/logs/silex_dev.log', 14 | )); 15 | 16 | $app->register(new WebProfilerServiceProvider(), array( 17 | 'profiler.cache_dir' => __DIR__.'/../var/cache/profiler', 18 | )); -------------------------------------------------------------------------------- /config/prod.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../var/cache/twig'); -------------------------------------------------------------------------------- /resources/hosts: -------------------------------------------------------------------------------- 1 | 172.21.99.6 mydddblog.test www.mydddblog.test redis.mydddblog.test -------------------------------------------------------------------------------- /resources/httpd.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName mydddblog.test 3 | ServerAlias www.mydddblog.test 4 | DocumentRoot /var/www/cqrs-blog-engine/web 5 | DirectoryIndex "index_dev.php" 6 | 7 | 8 | AllowOverride None 9 | Order allow,deny 10 | Allow from All 11 | Options -MultiViews 12 | 13 | RewriteEngine On 14 | RewriteCond %{REQUEST_FILENAME} !-f 15 | RewriteRule ^ index_dev.php [QSA,L] 16 | 17 | 18 | ErrorLog /var/log/apache2/mydddblog.test.log 19 | CustomLog /var/log/apache2/mydddblog.test.log combined 20 | -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.CommentId.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\CommentId: 2 | exclusion_policy: ALL 3 | properties: 4 | commentId: 5 | type: string 6 | serialized_name: comment_id -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.CommentWasAdded.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\CommentWasAdded: 2 | exclusion_policy: ALL 3 | 4 | properties: 5 | postId: 6 | type: CQRSBlog\BlogEngine\DomainModel\PostId 7 | serialized_name: post_id 8 | 9 | commentId: 10 | type: CQRSBlog\BlogEngine\DomainModel\CommentId 11 | serialized_name: comment_id 12 | 13 | comment: 14 | type: string -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.PostContentWasChanged.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\PostContentWasChanged: 2 | exclusion_policy: ALL 3 | 4 | properties: 5 | postId: 6 | type: CQRSBlog\BlogEngine\DomainModel\PostId 7 | serialized_name: post_id 8 | 9 | content: 10 | type: string -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.PostId.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\PostId: 2 | exclusion_policy: ALL 3 | properties: 4 | postId: 5 | type: string 6 | serialized_name: post_id -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.PostTitleWasChanged.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\PostTitleWasChanged: 2 | exclusion_policy: ALL 3 | 4 | properties: 5 | postId: 6 | type: CQRSBlog\BlogEngine\DomainModel\PostId 7 | serialized_name: post_id 8 | 9 | title: 10 | type: string -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.PostWasCreated.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\PostWasCreated: 2 | exclusion_policy: ALL 3 | 4 | properties: 5 | postId: 6 | type: CQRSBlog\BlogEngine\DomainModel\PostId 7 | serialized_name: post_id 8 | 9 | title: 10 | type: string 11 | 12 | content: 13 | type: string 14 | 15 | state: 16 | type: integer -------------------------------------------------------------------------------- /resources/mapping/serializer/CQRSBlog.BlogEngine.DomainModel.PostWasPublished.yml: -------------------------------------------------------------------------------- 1 | CQRSBlog\BlogEngine\DomainModel\PostWasPublished: 2 | exclusion_policy: ALL 3 | 4 | properties: 5 | postId: 6 | type: CQRSBlog\BlogEngine\DomainModel\PostId 7 | serialized_name: post_id -------------------------------------------------------------------------------- /resources/provisioning.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ ! -f ~/.machine-bootstraped ]; then 4 | 5 | echo 'Updating APT' 6 | apt-get -y -qq update > /dev/null 2>&1 7 | echo 'Done.' 8 | 9 | echo 'Installing DOTDEB sources' 10 | echo 'deb http://packages.dotdeb.org wheezy all 11 | deb-src http://packages.dotdeb.org wheezy all 12 | 13 | deb http://packages.dotdeb.org wheezy-php55 all 14 | deb-src http://packages.dotdeb.org wheezy-php55 all' > /etc/apt/sources.list.d/dotdeb.list 15 | 16 | wget -q http://www.dotdeb.org/dotdeb.gpg > /dev/null 2>&1 17 | apt-key add dotdeb.gpg > /dev/null 2>&1 18 | apt-get -y -qq update > /dev/null 2>&1 19 | echo 'Done.' 20 | 21 | echo 'Installing base packages' 22 | apt-get -y -qq install git build-essential gettext pkg-config redis-server apache2 apache2-mpm-prefork php5 php5-dev php-pear php5-curl php5-xdebug php5-mysql php5-cli php5-redis > /dev/null 2>&1 23 | echo 'Done.' 24 | 25 | echo 'Installing PHPREDMIN' 26 | wget https://github.com/sasanrose/phpredmin/archive/master.zip > /dev/null 2>&1 27 | unzip -qq master.zip > /dev/null 2>&1 28 | mv phpredmin-master /var/www/phpredmin 29 | chown -R vagrant:vagrant /var/www/phpredmin 30 | echo '# phpredmin - Simple web interface to manage and monitor your Redis 31 | # 32 | # Allows only localhost by default 33 | 34 | ServerName redis.mydddblog.test 35 | DocumentRoot /var/www/phpredmin/public 36 | 37 | 38 | AllowOverride All 39 | Order Deny,Allow 40 | Deny from All 41 | Allow from 127.0.0.1 42 | Allow from ::1 43 | Allow from 172.21.99.1 44 | 45 | 46 | ErrorLog /var/log/apache2/redis.mydddblog.test.log 47 | CustomLog /var/log/apache2/redis.mydddblog.test.log combined 48 | ' > /etc/apache2/sites-available/phpredmin 49 | a2ensite phpredmin 50 | echo 'Done.' 51 | 52 | echo 'Enabling & Configuring PHP APACHE MODULE' 53 | a2enmod php5 > /dev/null 2>&1 54 | echo 'date.timezone = "Europe/Madrid"' >> /etc/php5/apache2/php.ini 55 | echo 'Done.' 56 | 57 | echo 'Configuring Xdebug' 58 | echo ";Debugging 59 | xdebug.remote_enable = 1; 60 | xdebug.remote_connect_back = 1; 61 | xdebug.remote_autostart = 1; 62 | xdebug.remote_port = 9000; 63 | 64 | ;Profiling 65 | xdebug.profiler_enable = 0; 66 | xdebug.profiler_enable_trigger = 1; 67 | xdebug.profiler_output_dir = \"/tmp/xdebug\";" >> /etc/php5/mods-available/xdebug.ini 68 | echo 'Done.' 69 | 70 | echo 'Configuring APACHE HTTPD server' 71 | a2enmod rewrite > /dev/null 2>&1 72 | a2dissite default > /dev/null 2>&1 73 | sed -i 's/export APACHE_RUN_USER=www\-data/export APACHE_RUN_USER=vagrant/g' /etc/apache2/envvars > /dev/null 2>&1 74 | sed -i 's/export APACHE_RUN_GROUP=www\-data/export APACHE_RUN_GROUP=vagrant/g' /etc/apache2/envvars > /dev/null 2>&1 75 | ln -f -s /var/www/cqrs-blog-engine/resources/httpd.conf /etc/apache2/sites-available/mydddblog > /dev/null 2>&1 76 | a2ensite mydddblog > /dev/null 2>&1 77 | chown -R vagrant:vagrant /var/lock/apache2/ > /dev/null 2>&1 78 | echo 'Done.' 79 | 80 | echo 'Setting up the application environment' 81 | chown -R vagrant:vagrant var/cache/ > /dev/null 2>&1 82 | echo 'Done.' 83 | 84 | touch ~/.machine-bootstraped 85 | fi 86 | 87 | echo 'Starting up development services' 88 | service apache2 restart > /dev/null 2>&1 89 | service redis-server restart > /dev/null 2>&1 90 | echo 'Done.' -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/CommentCommand.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 20 | $this->postId = $postId; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getPostId() 27 | { 28 | return $this->postId; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getComment() 35 | { 36 | return $this->comment; 37 | } 38 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/CommentHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 19 | } 20 | 21 | public function handle(CommentCommand $aCommentCommand) 22 | { 23 | $aNewPost = $this->postRepository->get( 24 | PostId::fromString($aCommentCommand->getPostId()) 25 | ); 26 | 27 | $aNewPost->comment($aCommentCommand->getComment()); 28 | 29 | $this->postRepository->add($aNewPost); 30 | } 31 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/CreatePostCommand.php: -------------------------------------------------------------------------------- 1 | content = $content; 13 | $this->title = $title; 14 | } 15 | 16 | /** 17 | * @return mixed 18 | */ 19 | public function getContent() 20 | { 21 | return $this->content; 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public function getTitle() 28 | { 29 | return $this->title; 30 | } 31 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/CreatePostHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 20 | } 21 | 22 | public function handle(CreatePostCommand $aCreatePostCommand) 23 | { 24 | $aNewPost = Post::create( 25 | PostId::generate(), 26 | $aCreatePostCommand->getTitle(), 27 | $aCreatePostCommand->getContent() 28 | ); 29 | 30 | $this->postRepository->add($aNewPost); 31 | } 32 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/PublishPostCommand.php: -------------------------------------------------------------------------------- 1 | id = $id; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getId() 21 | { 22 | return $this->id; 23 | } 24 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/PublishPostHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 19 | } 20 | 21 | public function handle(PublishPostCommand $aPublishPostCommand) 22 | { 23 | $aPost = $this->postRepository->get(PostId::fromString($aPublishPostCommand->getId())); 24 | 25 | $aPost->publish(); 26 | 27 | $this->postRepository->add($aPost); 28 | } 29 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/UpdatePostCommand.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 14 | $this->content = $content; 15 | $this->title = $title; 16 | } 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function getPostId() 22 | { 23 | return $this->postId; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getContent() 30 | { 31 | return $this->content; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getTitle() 38 | { 39 | return $this->title; 40 | } 41 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Command/UpdatePostHandler.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 19 | } 20 | 21 | public function handle(UpdatePostCommand $anUpdatePostCommand) 22 | { 23 | $aPost = $this->postRepository->get( 24 | PostId::fromString($anUpdatePostCommand->getPostId()) 25 | ); 26 | 27 | $aPost->changeTitle($anUpdatePostCommand->getTitle()); 28 | $aPost->changeContent($anUpdatePostCommand->getContent()); 29 | 30 | $this->postRepository->add($aPost); 31 | } 32 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/Comment.php: -------------------------------------------------------------------------------- 1 | id = $id; 20 | $this->comment = $comment; 21 | } 22 | 23 | public static function create($aCommentId, $aComment) 24 | { 25 | return new Comment($aCommentId, $aComment); 26 | } 27 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/CommentId.php: -------------------------------------------------------------------------------- 1 | commentId = $aCommentId; 14 | } 15 | 16 | /** 17 | * Returns a string that can be parsed by fromString() 18 | * @return string 19 | */ 20 | public function __toString() 21 | { 22 | return (string) $this->commentId; 23 | } 24 | 25 | public function equals(CommentId $other) 26 | { 27 | return $this->commentId === $other->commentId; 28 | } 29 | 30 | public static function generate() 31 | { 32 | return new CommentId( 33 | (string) Uuid::uuid1() 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/CommentWasAdded.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 28 | $this->commentId = $commentId; 29 | $this->comment = $comment; 30 | } 31 | 32 | /** 33 | * The Aggregate this event belongs to. 34 | * 35 | * @return IdentifiesAggregate 36 | */ 37 | public function getAggregateId() 38 | { 39 | return $this->postId; 40 | } 41 | 42 | /** 43 | * @return Comment 44 | */ 45 | public function getComment() 46 | { 47 | return $this->comment; 48 | } 49 | 50 | /** 51 | * @return CommentId 52 | */ 53 | public function getCommentId() 54 | { 55 | return $this->commentId; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/Post.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 50 | $this->content = $content; 51 | $this->title = $title; 52 | $this->state = $state; 53 | } 54 | 55 | /** 56 | * @param $aPostId 57 | * @return Post 58 | */ 59 | private static function createEmptyPostWith($aPostId) 60 | { 61 | return new Post($aPostId, '', '', static::STATE_DRAFT); 62 | } 63 | 64 | /** 65 | * Get all the Domain Events that were recorded since the last time it was cleared, or since it was 66 | * restored from persistence. This does not include events that were recorded prior. 67 | * 68 | * @return DomainEvents 69 | */ 70 | public function getRecordedEvents() 71 | { 72 | return new DomainEvents($this->recordedEvents); 73 | } 74 | 75 | /** 76 | * Clears the record of new Domain Events. This doesn't clear the history of the object. 77 | * @return void 78 | */ 79 | public function clearRecordedEvents() 80 | { 81 | $this->recordedEvents = []; 82 | } 83 | 84 | public static function create($aPostId, $aTitle, $aContent) 85 | { 86 | $aNewPost = new Post($aPostId, $aTitle, $aContent, static::STATE_DRAFT); 87 | 88 | $aNewPost->recordThat( 89 | new PostWasCreated($aPostId, $aTitle, $aContent, static::STATE_DRAFT) 90 | ); 91 | 92 | return $aNewPost; 93 | } 94 | 95 | public function publish() 96 | { 97 | $this->applyAndRecordThat( 98 | new PostWasPublished($this->postId) 99 | ); 100 | } 101 | 102 | public function changeTitle($aNewTitle) 103 | { 104 | $this->applyAndRecordThat( 105 | new PostTitleWasChanged($this->postId, $aNewTitle) 106 | ); 107 | } 108 | 109 | public function changeContent($aNewContent) 110 | { 111 | $this->applyAndRecordThat( 112 | new PostContentWasChanged($this->postId, $aNewContent) 113 | ); 114 | } 115 | 116 | public function comment($aNewComment) 117 | { 118 | $this->applyAndRecordThat( 119 | new CommentWasAdded($this->postId, CommentId::generate(), $aNewComment) 120 | ); 121 | } 122 | 123 | private function recordThat(DomainEvent $aDomainEvent) 124 | { 125 | $this->recordedEvents[] = $aDomainEvent; 126 | } 127 | 128 | private function applyAndRecordThat(DomainEvent $aDomainEvent) 129 | { 130 | $this->recordThat($aDomainEvent); 131 | 132 | $this->apply($aDomainEvent); 133 | } 134 | 135 | /** 136 | * Allow to reconstitute an aggregate from an aggregate events history and an initial state 137 | * 138 | * @param AggregateHistory $anAggregateHistory 139 | * 140 | * @return RecordsEvents 141 | */ 142 | public static function reconstituteFrom(AggregateHistory $anAggregateHistory) 143 | { 144 | $aPost = static::createEmptyPostWith($anAggregateHistory->getAggregateId()); 145 | 146 | foreach ($anAggregateHistory as $anEvent) { 147 | $aPost->apply($anEvent); 148 | } 149 | 150 | return $aPost; 151 | } 152 | 153 | private function apply($anEvent) 154 | { 155 | $method = 'apply' . ClassFunctions::short($anEvent); 156 | $this->$method($anEvent); 157 | } 158 | 159 | private function applyPostWasCreated(PostWasCreated $event) 160 | { 161 | $this->title = $event->getTitle(); 162 | $this->content = $event->getContent(); 163 | } 164 | 165 | private function applyPostWasPublished(PostWasPublished $event) 166 | { 167 | $this->state = static::STATE_PUBLISHED; 168 | } 169 | 170 | private function applyPostTitleWasChanged(PostTitleWasChanged $event) 171 | { 172 | $this->title = $event->getTitle(); 173 | } 174 | 175 | private function applyPostContentWasChanged(PostContentWasChanged $event) 176 | { 177 | $this->content = $event->getContent(); 178 | } 179 | 180 | private function applyCommentWasAdded(CommentWasAdded $event) 181 | { 182 | $this->comments[] = Comment::create($event->getCommentId(), $event->getComment()); 183 | } 184 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostContentWasChanged.php: -------------------------------------------------------------------------------- 1 | content = $content; 23 | $this->postId = $postId; 24 | } 25 | 26 | /** 27 | * The Aggregate this event belongs to. 28 | * 29 | * @return IdentifiesAggregate 30 | */ 31 | public function getAggregateId() 32 | { 33 | return $this->postId; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getContent() 40 | { 41 | return $this->content; 42 | } 43 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostId.php: -------------------------------------------------------------------------------- 1 | postId = $aPostId; 15 | } 16 | 17 | /** 18 | * Creates an identifier object from a string representation 19 | * @param string $aPostId 20 | * @return IdentifiesAggregate 21 | */ 22 | public static function fromString($aPostId) 23 | { 24 | return new PostId($aPostId); 25 | } 26 | 27 | /** 28 | * Returns a string that can be parsed by fromString() 29 | * @return string 30 | */ 31 | public function __toString() 32 | { 33 | return (string) $this->postId; 34 | } 35 | 36 | /** 37 | * Compares the object to another IdentifiesAggregate object. Returns true if both have the same type and value. 38 | * @param $other 39 | * @return boolean 40 | */ 41 | public function equals(IdentifiesAggregate $other) 42 | { 43 | return 44 | $other instanceof PostId 45 | && $this->postId === $other->postId 46 | ; 47 | } 48 | 49 | public static function generate() 50 | { 51 | return new PostId( 52 | (string) Uuid::uuid1() 53 | ); 54 | } 55 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostProjection.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 23 | $this->title = $title; 24 | } 25 | 26 | /** 27 | * The Aggregate this event belongs to. 28 | * @return IdentifiesAggregate 29 | */ 30 | public function getAggregateId() 31 | { 32 | return $this->postId; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getTitle() 39 | { 40 | return $this->title; 41 | } 42 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostView.php: -------------------------------------------------------------------------------- 1 | id = $id; 30 | $this->title = $title; 31 | $this->content = $content; 32 | $this->comments = $comments; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getContent() 39 | { 40 | return $this->content; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getTitle() 55 | { 56 | return $this->title; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getComments() 63 | { 64 | return $this->comments; 65 | } 66 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostViewRepository.php: -------------------------------------------------------------------------------- 1 | postId = $aggregateId; 18 | $this->title = $title; 19 | $this->content = $content; 20 | $this->state = $state; 21 | } 22 | 23 | /** 24 | * The Aggregate this event belongs to. 25 | * @return IdentifiesAggregate 26 | */ 27 | public function getAggregateId() 28 | { 29 | return $this->postId; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getContent() 36 | { 37 | return $this->content; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getTitle() 44 | { 45 | return $this->title; 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | public function getState() 52 | { 53 | return $this->state; 54 | } 55 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/DomainModel/PostWasPublished.php: -------------------------------------------------------------------------------- 1 | postId = $aPostId; 18 | } 19 | 20 | /** 21 | * The Aggregate this event belongs to. 22 | * 23 | * @return IdentifiesAggregate 24 | */ 25 | public function getAggregateId() 26 | { 27 | return $this->postId; 28 | } 29 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/EventStore/EventStore.php: -------------------------------------------------------------------------------- 1 | eventsCollection = $eventsCollection; 28 | $this->serializer = $serializer; 29 | } 30 | 31 | /** 32 | * @param DomainEvents $events 33 | * @return void 34 | */ 35 | public function commit(DomainEvents $events) 36 | { 37 | foreach ($events as $event) { 38 | $this->eventsCollection->insert([ 39 | 'aggregate_id' => (string) $event->getAggregateId(), 40 | 'type' => get_class($event), 41 | 'created_at' => new MongoDate(), 42 | 'data' => $this->serializer->serialize($event, 'json') 43 | ]); 44 | } 45 | } 46 | 47 | /** 48 | * @param IdentifiesAggregate $id 49 | * @return AggregateHistory 50 | */ 51 | public function getAggregateHistoryFor(IdentifiesAggregate $id) 52 | { 53 | $rawEvents = $this->eventsCollection->find(['aggregate_id' => (string) $id]); 54 | 55 | $events = []; 56 | 57 | foreach ($rawEvents as $rawEvent) { 58 | $events[] = $this->serializer->deserialize( 59 | $rawEvent['data'], 60 | $rawEvent['type'], 61 | 'json' 62 | ); 63 | } 64 | 65 | return new AggregateHistory($id, $events); 66 | } 67 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/EventStore/PDOEventStore.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 29 | $this->serializer = $serializer; 30 | } 31 | 32 | /** 33 | * @param DomainEvents $events 34 | * @return void 35 | */ 36 | public function commit(DomainEvents $events) 37 | { 38 | $stmt = $this->pdo->prepare( 39 | 'INSERT INTO events (aggregate_id, `type`, created_at, `data`) 40 | VALUES (:aggregate_id, :type, :created_at, :data)' 41 | ); 42 | 43 | foreach ($events as $event) { 44 | $stmt->execute([ 45 | ':aggregate_id' => (string) $event->getAggregateId(), 46 | ':type' => get_class($event), 47 | ':created_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), 48 | ':data' => $this->serializer->serialize($event, 'json') 49 | ]); 50 | } 51 | } 52 | 53 | /** 54 | * @param IdentifiesAggregate $id 55 | * @return AggregateHistory 56 | */ 57 | public function getAggregateHistoryFor(IdentifiesAggregate $id) 58 | { 59 | $stmt = $this->pdo->query( 60 | 'SELECT * FROM events WHERE aggregate_id = :aggregate_id' 61 | ); 62 | $stmt->execute([':aggregate_id' => (string) $id]); 63 | 64 | $events = []; 65 | 66 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 67 | $events[] = $this->serializer->deserialize( 68 | $row['data'], 69 | $row['type'], 70 | 'json' 71 | ); 72 | } 73 | 74 | $stmt->closeCursor(); 75 | 76 | return new AggregateHistory($id, $events); 77 | } 78 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/EventStore/PostRepository.php: -------------------------------------------------------------------------------- 1 | eventStore = $eventStore; 27 | $this->postProjection = $postProjection; 28 | } 29 | 30 | /** 31 | * @param IdentifiesAggregate $aggregateId 32 | * @return IsEventSourced 33 | */ 34 | public function get(IdentifiesAggregate $aggregateId) 35 | { 36 | $eventStream = $this->eventStore->getAggregateHistoryFor($aggregateId); 37 | 38 | return Post::reconstituteFrom($eventStream); 39 | } 40 | 41 | /** 42 | * @param RecordsEvents $aggregate 43 | * @return void 44 | */ 45 | public function add(RecordsEvents $aggregate) 46 | { 47 | $events = $aggregate->getRecordedEvents(); 48 | $this->eventStore->commit($events); 49 | $this->postProjection->project($events); 50 | 51 | $aggregate->clearRecordedEvents(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/EventStore/RedisEventStore.php: -------------------------------------------------------------------------------- 1 | predis = $predis; 27 | $this->serializer = $serializer; 28 | } 29 | 30 | public function commit(DomainEvents $events) 31 | { 32 | foreach ($events as $event) { 33 | $eventType = get_class($event); 34 | $data = $this->serializer->serialize($event, 'json'); 35 | 36 | $this->predis->rpush( 37 | $this->computeHashFor($event->getAggregateId()), 38 | $this->serializer->serialize([ 39 | 'type' => $eventType, 40 | 'created_on' => (new DateTimeImmutable())->format('YmdHis'), 41 | 'data' => $data 42 | ], 'json') 43 | ); 44 | } 45 | } 46 | 47 | public function getAggregateHistoryFor(IdentifiesAggregate $id) 48 | { 49 | $serializedEvents = $this->predis->lrange($this->computeHashFor($id), 0, -1); 50 | 51 | $eventStream = []; 52 | 53 | foreach ($serializedEvents as $serializedEvent) { 54 | $eventData = $this->serializer->deserialize($serializedEvent, 'array', 'json'); 55 | $eventStream[] = $this->serializer->deserialize($eventData['data'], $eventData['type'], 'json'); 56 | } 57 | 58 | return new AggregateHistory($id, $eventStream); 59 | } 60 | 61 | private function computeHashFor(IdentifiesAggregate $anAggregateId) 62 | { 63 | return sprintf('events:%s', $anAggregateId); 64 | } 65 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/MongoDb/PostViewRepository.php: -------------------------------------------------------------------------------- 1 | postsComments = $postsComments; 19 | } 20 | 21 | /** 22 | * Get a post view by its id 23 | * 24 | * @param string $id 25 | * 26 | * @return PostView 27 | */ 28 | public function get($id) 29 | { 30 | $rawPost = $this->postsComments->findOne(['post_id' => $id]); 31 | 32 | return $this->newPostView($rawPost); 33 | } 34 | 35 | /** 36 | * Get all of the post views 37 | * 38 | * @return PostView[] 39 | */ 40 | public function all() 41 | { 42 | $rawPosts = $this->postsComments->find(); 43 | 44 | $posts = []; 45 | 46 | foreach ($rawPosts as $rawPost) { 47 | $posts[] = $this->newPostView($rawPost); 48 | } 49 | 50 | return $posts; 51 | } 52 | 53 | private function newPostView($rawPost) 54 | { 55 | return new PostView( 56 | $rawPost['post_id'], 57 | $rawPost['title'], 58 | $rawPost['content'], 59 | isset($rawPost['comments']) ? $rawPost['comments'] : [] 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/PDO/PostViewRepository.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 19 | } 20 | 21 | /** 22 | * Get a post view by its id 23 | * 24 | * @param string $id 25 | * 26 | * @return PostView 27 | */ 28 | public function get($id) 29 | { 30 | $stmt = $this->pdo->query('SELECT * FROM posts_with_comments WHERE post_id = :post_id'); 31 | $stmt->execute([ 32 | ':post_id' => $id 33 | ]); 34 | 35 | $title = $content = null; 36 | $comments = []; 37 | 38 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 39 | if (null === $title) { 40 | $title = $row['TITLE']; 41 | $content = $row['CONTENT']; 42 | } 43 | 44 | $comments[] = [ 45 | 'comment_id' => $row['COMMENT_ID'], 46 | 'comment' => $row['COMMENT'] 47 | ]; 48 | } 49 | 50 | return new PostView( 51 | $id, 52 | $title, 53 | $content, 54 | $comments 55 | ); 56 | } 57 | 58 | /** 59 | * Get all of the post views 60 | * 61 | * @return PostView[] 62 | */ 63 | public function all() 64 | { 65 | $stmt = $this->pdo->query('SELECT * FROM posts'); 66 | $stmt->execute(); 67 | 68 | $posts = []; 69 | 70 | while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { 71 | $posts[] = new PostView( 72 | $row['POST_ID'], 73 | $row['TITLE'], 74 | $row['CONTENT'], 75 | [] 76 | ); 77 | } 78 | 79 | return $posts; 80 | } 81 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Persistence/Redis/PostViewRepository.php: -------------------------------------------------------------------------------- 1 | predis = $predis; 25 | $this->serializer = $serializer; 26 | } 27 | 28 | /** 29 | * Finds a post view by its id 30 | * 31 | * @param string $id 32 | * 33 | * @return PostView 34 | */ 35 | public function get($id) 36 | { 37 | $rawPostView = $this->predis->hgetall(sprintf('posts:%s', $id)); 38 | 39 | return new PostView( 40 | $id, 41 | $rawPostView['title'], 42 | $rawPostView['content'], 43 | isset($rawPostView['comments']) ? $this->serializer->deserialize($rawPostView['comments'], 'array', 'json') : [] 44 | ); 45 | } 46 | 47 | /** 48 | * Get all of the post views 49 | * 50 | * @return PostView[] 51 | */ 52 | public function all() 53 | { 54 | $postIds = $this->predis->lrange('posts', 0, -1); 55 | 56 | if (empty($postIds)) { 57 | return []; 58 | } 59 | 60 | $posts = []; 61 | 62 | foreach ($postIds as $postId) { 63 | $posts[] = $this->get(explode(':', $postId)[1]); 64 | } 65 | 66 | return $posts; 67 | } 68 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Projection/BaseProjection.php: -------------------------------------------------------------------------------- 1 | $projectMethod($event); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Projection/MongoDb/PostProjection.php: -------------------------------------------------------------------------------- 1 | postsCollection = $postsCollection; 25 | } 26 | 27 | /** 28 | * Projects a posts creation event 29 | * 30 | * @param PostWasCreated $event 31 | * 32 | * @return void 33 | */ 34 | public function projectPostWasCreated(PostWasCreated $event) 35 | { 36 | if ($this->postsCollection->count(['post_id' => (string) $event->getAggregateId()]) > 0) { 37 | return; 38 | } 39 | 40 | $this->postsCollection->insert([ 41 | 'post_id' => (string) $event->getAggregateId(), 42 | 'title' => $event->getTitle(), 43 | 'content' => $event->getContent(), 44 | 'state' => $event->getState() 45 | ]); 46 | } 47 | 48 | /** 49 | * Projects when a post was published 50 | * 51 | * @param PostWasPublished $event 52 | * 53 | * @return void 54 | */ 55 | public function projectPostWasPublished(PostWasPublished $event) 56 | { 57 | $post = $this->postsCollection->findOne(['post_id' => (string) $event->getAggregateId()]); 58 | 59 | if (null === $post || Post::STATE_PUBLISHED === $post['state']) { 60 | return; 61 | } 62 | 63 | $post['state'] = Post::STATE_PUBLISHED; 64 | 65 | $this->postsCollection->save($post); 66 | } 67 | 68 | /** 69 | * Projects when a post title was changed 70 | * 71 | * @param PostTitleWasChanged $event 72 | * 73 | * @return void 74 | */ 75 | public function projectPostTitleWasChanged(PostTitleWasChanged $event) 76 | { 77 | $post = $this->postsCollection->findOne(['post_id' => (string) $event->getAggregateId()]); 78 | 79 | if (null === $post || $event->getTitle() === $post['title']) { 80 | return; 81 | } 82 | 83 | $post['title'] = $event->getTitle(); 84 | 85 | $this->postsCollection->save($post); 86 | } 87 | 88 | /** 89 | * Projects when a post content was changed 90 | * 91 | * @param PostContentWasChanged $event 92 | * 93 | * @return void 94 | */ 95 | public function projectPostContentWasChanged(PostContentWasChanged $event) 96 | { 97 | $post = $this->postsCollection->findOne(['post_id' => (string) $event->getAggregateId()]); 98 | 99 | if (null === $post || Post::STATE_PUBLISHED === $post['content']) { 100 | return; 101 | } 102 | 103 | $post['content'] = $event->getContent(); 104 | 105 | $this->postsCollection->save($post); 106 | } 107 | 108 | /** 109 | * Projects when a comment is added 110 | * 111 | * @param CommentWasAdded $event 112 | * 113 | * @return void 114 | */ 115 | public function projectCommentWasAdded(CommentWasAdded $event) 116 | { 117 | $post = $this->postsCollection->findOne(['post_id' => (string) $event->getAggregateId()]); 118 | 119 | if (!isset($post['comments'])) { 120 | $post['comments'] = []; 121 | } 122 | 123 | $commentAlreadyExists = count(array_filter($post['comments'], function ($comment) use ($event) { 124 | return $comment['comment_id'] === $event->getCommentId(); 125 | })) > 0; 126 | 127 | if ($commentAlreadyExists) { 128 | return; 129 | } 130 | 131 | $post['comments'] = [ 132 | 'comment_id' => (string) $event->getCommentId(), 133 | 'comment' => $event->getComment() 134 | ]; 135 | 136 | $this->postsCollection->save($post); 137 | } 138 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Projection/PDO/PostProjection.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 25 | } 26 | 27 | /** 28 | * Projects a posts creation event 29 | * 30 | * @param PostWasCreated $event 31 | * 32 | * @return void 33 | */ 34 | public function projectPostWasCreated(PostWasCreated $event) 35 | { 36 | $stmt = $this->pdo->prepare( 37 | 'INSERT INTO posts (post_id, title, content, state) 38 | VALUES (:post_id, :title, :content, :state)' 39 | ); 40 | 41 | $stmt->execute([ 42 | ':post_id' => $event->getAggregateId(), 43 | ':title' => $event->getTitle(), 44 | ':content' => $event->getContent(), 45 | ':state' => $event->getState() 46 | ]); 47 | } 48 | 49 | /** 50 | * Projects when a post was published 51 | * 52 | * @param PostWasPublished $event 53 | * 54 | * @return void 55 | */ 56 | public function projectPostWasPublished(PostWasPublished $event) 57 | { 58 | $stmt = $this->pdo->prepare( 59 | 'UPDATE posts SET state = :state WHERE post_id = :post_id' 60 | ); 61 | 62 | $stmt->execute([ 63 | ':state' => Post::STATE_PUBLISHED, 64 | ':post_id' => $event->getAggregateId() 65 | ]); 66 | } 67 | 68 | /** 69 | * Projects when a post title was changed 70 | * 71 | * @param PostTitleWasChanged $event 72 | * 73 | * @return void 74 | */ 75 | public function projectPostTitleWasChanged(PostTitleWasChanged $event) 76 | { 77 | $stmt = $this->pdo->prepare( 78 | 'UPDATE posts SET title = :title WHERE post_id = :post_id' 79 | ); 80 | 81 | $stmt->execute([ 82 | ':title' => $event->getTitle(), 83 | ':post_id' => $event->getAggregateId() 84 | ]); 85 | } 86 | 87 | /** 88 | * Projects when a post content was changed 89 | * 90 | * @param PostContentWasChanged $event 91 | * 92 | * @return void 93 | */ 94 | public function projectPostContentWasChanged(PostContentWasChanged $event) 95 | { 96 | $stmt = $this->pdo->prepare( 97 | 'UPDATE posts SET content = :content WHERE post_id = :post_id' 98 | ); 99 | 100 | $stmt->execute([ 101 | ':content' => $event->getContent(), 102 | ':post_id' => $event->getAggregateId() 103 | ]); 104 | } 105 | 106 | /** 107 | * Projects when a comment is added 108 | * 109 | * @param CommentWasAdded $event 110 | * 111 | * @return void 112 | */ 113 | public function projectCommentWasAdded(CommentWasAdded $event) 114 | { 115 | $stmt = $this->pdo->query('SELECT * FROM posts WHERE post_id = :post_id'); 116 | $stmt->bindParam(':post_id', $event->getAggregateId()); 117 | $post = $stmt->fetch(PDO::FETCH_ASSOC); 118 | $stmt->closeCursor(); 119 | 120 | $stmt = $this->pdo->prepare( 121 | 'INSERT INTO posts_with_comments (post_id, comment_id, title, content, state, comment) 122 | VALUES (:post_id, :comment_id, :title, :content, :state, :comment)' 123 | ); 124 | 125 | $stmt->execute([ 126 | ':post_id' => $event->getAggregateId(), 127 | ':comment_id' => $event->getCommentId(), 128 | ':title' => $post['title'], 129 | ':content' => $post['content'], 130 | ':state' => $post['state'], 131 | ':comment' => $post['comment'] 132 | ]); 133 | } 134 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Infrastructure/Projection/Redis/PostProjection.php: -------------------------------------------------------------------------------- 1 | predis = $predis; 31 | $this->serializer = $serializer; 32 | } 33 | 34 | /** 35 | * Projects a posts creation event 36 | * 37 | * @param PostWasCreated $event 38 | * 39 | * @return void 40 | */ 41 | public function projectPostWasCreated(PostWasCreated $event) 42 | { 43 | $anAggregateId = $event->getAggregateId(); 44 | 45 | $hash = $this->computePostHashFor($anAggregateId); 46 | 47 | $this->predis->hmset( 48 | $hash, 49 | [ 50 | 'title' => $event->getTitle(), 51 | 'content' => $event->getContent(), 52 | 'state' => $event->getState() 53 | ] 54 | ); 55 | 56 | $this->predis->rpush('posts', $hash); 57 | } 58 | 59 | /** 60 | * Projects when a post was published 61 | * 62 | * @param PostWasPublished $event 63 | * 64 | * @return void 65 | */ 66 | public function projectPostWasPublished(PostWasPublished $event) 67 | { 68 | $this->predis->hset( 69 | $this->computePostHashFor($event->getAggregateId()), 70 | 'state', 71 | Post::STATE_PUBLISHED 72 | ); 73 | } 74 | 75 | /** 76 | * Projects when a post title was changed 77 | * 78 | * @param PostTitleWasChanged $event 79 | * 80 | * @return void 81 | */ 82 | public function projectPostTitleWasChanged(PostTitleWasChanged $event) 83 | { 84 | $this->predis->hset( 85 | $this->computePostHashFor($event->getAggregateId()), 86 | 'title', 87 | $event->getTitle() 88 | ); 89 | } 90 | 91 | /** 92 | * Projects when a post content was changed 93 | * 94 | * @param PostContentWasChanged $event 95 | * 96 | * @return void 97 | */ 98 | public function projectPostContentWasChanged(PostContentWasChanged $event) 99 | { 100 | $this->predis->hset( 101 | $this->computePostHashFor($event->getAggregateId()), 102 | 'content', 103 | $event->getContent() 104 | ); 105 | } 106 | 107 | /** 108 | * @param $anAggregateId 109 | * @return string 110 | */ 111 | protected function computePostHashFor($anAggregateId) 112 | { 113 | return sprintf('posts:%s', $anAggregateId); 114 | } 115 | 116 | /** 117 | * Projects when a comment is added 118 | * 119 | * @param CommentWasAdded $event 120 | * 121 | * @return void 122 | */ 123 | public function projectCommentWasAdded(CommentWasAdded $event) 124 | { 125 | $rawComments = $this->predis->hget( 126 | $this->computePostHashFor($event->getAggregateId()), 127 | 'comments' 128 | ); 129 | 130 | $comments = []; 131 | 132 | if (null !== $rawComments) { 133 | $comments = $this->serializer->deserialize( 134 | $rawComments, 135 | 'array', 136 | 'json' 137 | ); 138 | } 139 | 140 | $comments[] = [ 141 | 'commentId' => (string) $event->getCommentId(), 142 | 'comment' => $event->getComment() 143 | ]; 144 | 145 | $this->predis->hset( 146 | $this->computePostHashFor($event->getAggregateId()), 147 | 'comments', 148 | $this->serializer->serialize($comments, 'json') 149 | ); 150 | } 151 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Query/AllPostsQuery.php: -------------------------------------------------------------------------------- 1 | postRepository = $postRepository; 17 | } 18 | 19 | public function handle(AllPostsQuery $anAllPostsQuery) 20 | { 21 | return $this->postRepository->all(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Query/PostQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getId() 21 | { 22 | return $this->id; 23 | } 24 | } -------------------------------------------------------------------------------- /src/CQRSBlog/BlogEngine/Query/PostQueryHandler.php: -------------------------------------------------------------------------------- 1 | postViewRepository = $postViewRepository; 18 | } 19 | 20 | public function handle(PostQuery $aPostQuery) 21 | { 22 | return $this->postViewRepository->get($aPostQuery->getId()); 23 | } 24 | } -------------------------------------------------------------------------------- /src/CQRSBlog/Common/DomainModel/Projection.php: -------------------------------------------------------------------------------- 1 | commandHandlers[$anUnderscoredCommandClass])) { 16 | throw new HandlerNotFoundException(get_class($aCommand)); 17 | } 18 | 19 | $aCommandHandler = $this->commandHandlers[$anUnderscoredCommandClass]; 20 | $aCommandHandler->handle($aCommand); 21 | } 22 | 23 | public function register($aCommandHandler) 24 | { 25 | $aCommandClass = str_replace( 26 | [ 27 | '.handler', 28 | '_handler' 29 | ], 30 | [ 31 | '', 32 | '_command' 33 | ], 34 | ClassFunctions::underscore($aCommandHandler) 35 | ); 36 | 37 | $this->commandHandlers[$aCommandClass] = $aCommandHandler; 38 | } 39 | } -------------------------------------------------------------------------------- /src/CQRSBlog/Common/ServiceBus/HandlerNotFoundException.php: -------------------------------------------------------------------------------- 1 | queryHandlers[$anUnderscoredQueryClass])) { 16 | throw new HandlerNotFoundException(get_class($aQuery)); 17 | } 18 | 19 | $aQueryHandler = $this->queryHandlers[$anUnderscoredQueryClass]; 20 | return $aQueryHandler->handle($aQuery); 21 | } 22 | 23 | public function register($aQueryHandler) 24 | { 25 | $anUnderscoredQueryHandlerClass = ClassFunctions::underscore($aQueryHandler); 26 | $aQueryClass = str_replace( 27 | [ 28 | '.handler', 29 | '_handler' 30 | ], 31 | [ 32 | '', 33 | '' 34 | ], 35 | $anUnderscoredQueryHandlerClass 36 | ); 37 | 38 | $this->queryHandlers[$aQueryClass] = $aQueryHandler; 39 | } 40 | } -------------------------------------------------------------------------------- /src/app.php: -------------------------------------------------------------------------------- 1 | register(new UrlGeneratorServiceProvider()); 35 | $app->register(new ValidatorServiceProvider()); 36 | $app->register(new Silex\Provider\SessionServiceProvider()); 37 | $app->register(new ServiceControllerServiceProvider()); 38 | $app->register(new FormServiceProvider()); 39 | $app->register(new Silex\Provider\TranslationServiceProvider(), ['locale_fallbacks' => array('en')]); 40 | $app->register(new Silex\Provider\TwigServiceProvider(), ['twig.form.templates' => ['bootstrap.html.twig']]); 41 | $app->register(new Predis\Silex\PredisServiceProvider(), [ 42 | 'predis.parameters' => 'tcp://127.0.0.1:6379', 43 | 'predis.options' => ['profile' => '2.2'], 44 | ]); 45 | 46 | $app->register(new PdoServiceProvider('pdo'), [ 47 | 'pdo.server' => [ 48 | 'driver' => 'mysql', 49 | 'host' => 'localhost', 50 | 'dbname' => 'mydddblog', 51 | 'port' => 3306, 52 | 'user' => 'mydddblog', 53 | 'password' => 'myDddbl0g', 54 | ], 55 | 'pdo.options' => [ 56 | PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'" 57 | ], 58 | 'pdo.attributes' => array( 59 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 60 | ), 61 | ]); 62 | 63 | $app['mongo'] = $app->share(function($app) { 64 | return new MongoClient(); 65 | }); 66 | 67 | $app['twig'] = $app->share($app->extend('twig', function($twig) { 68 | $twig->addExtension(new BootstrapIconExtension); 69 | $twig->addExtension(new BootstrapLabelExtension); 70 | $twig->addExtension(new BootstrapBadgeExtension); 71 | $twig->addExtension(new BootstrapFormExtension); 72 | 73 | return $twig; 74 | })); 75 | 76 | $app['serializer'] = $app->share(function($app) { 77 | return 78 | JMS\Serializer\SerializerBuilder::create() 79 | ->setCacheDir(__DIR__ . '/../var/cache/serializer') 80 | ->setDebug($app['debug']) 81 | ->addMetadataDir(__DIR__ . '/../resources/mapping/serializer') 82 | ->build() 83 | ; 84 | }); 85 | 86 | 87 | /******************************************* 88 | * EVENT STORE CONFIGURATION 89 | *******************************************/ 90 | 91 | $app['event_store.redis'] = $app->share(function($app) { 92 | return new RedisEventStore($app['predis'], $app['serializer']); 93 | }); 94 | 95 | $app['event_store.mongodb'] = $app->share(function($app) { 96 | return new MongoDbEventStore($app['mongo']->dddblog->events, $app['serializer']); 97 | }); 98 | 99 | $app['event_store.pdo'] = $app->share(function($app) { 100 | return new PDOEventStore($app['pdo'], $app['serializer']); 101 | }); 102 | 103 | $app['event_store'] = $app->share(function($app) { 104 | return $app['event_store.redis']; 105 | }); 106 | 107 | /******************************************* 108 | * EVENT STORE REPOSITORIES 109 | *******************************************/ 110 | 111 | $app['post_repository'] = $app->share(function($app) { 112 | return new PostRepository($app['event_store'], $app['post_projection']); 113 | }); 114 | 115 | /******************************************* 116 | * PERSISTENCE CONFIGURATION 117 | *******************************************/ 118 | 119 | $app['post_view_repository.redis'] = $app->share(function($app) { 120 | return new RedisPostViewRepository($app['predis'], $app['serializer']); 121 | }); 122 | 123 | $app['post_view_repository.mongodb'] = $app->share(function($app) { 124 | return new MongoDbPostViewRepository($app['mongo']->dddblog->posts); 125 | }); 126 | 127 | $app['post_view_repository.pdo'] = $app->share(function($app) { 128 | return new PDOPostViewRepository($app['pdo']); 129 | }); 130 | 131 | $app['post_view_repository'] = $app->share(function($app) { 132 | return $app['post_view_repository.redis']; 133 | }); 134 | 135 | /******************************************* 136 | * READ MODEL PROJECTIONS 137 | *******************************************/ 138 | 139 | $app['post_projection.redis'] = $app->share(function($app) { 140 | return new RedisPostProjection($app['predis'], $app['serializer']); 141 | }); 142 | 143 | $app['post_projection.mongodb'] = $app->share(function($app) { 144 | return new MongoDbPostProjection($app['mongo']->dddblog->posts, $app['serializer']); 145 | }); 146 | 147 | $app['post_projection.pdo'] = $app->share(function($app) { 148 | return new PDOPostProjection($app['pdo'], $app['serializer']); 149 | }); 150 | 151 | $app['post_projection'] = $app->share(function($app) { 152 | return $app['post_projection.redis']; 153 | }); 154 | 155 | /******************************************* 156 | * COMMAND BUS 157 | *******************************************/ 158 | 159 | $app['command_bus'] = $app->share(function($app) { 160 | $commandBus = new CommandBus(); 161 | $commandBus->register(new CreatePostHandler($app['post_repository'])); 162 | $commandBus->register(new PublishPostHandler($app['post_repository'])); 163 | $commandBus->register(new UpdatePostHandler($app['post_repository'])); 164 | $commandBus->register(new CommentHandler($app['post_repository'])); 165 | 166 | return $commandBus; 167 | }); 168 | 169 | /******************************************* 170 | * QUERY BUS 171 | *******************************************/ 172 | 173 | $app['query_bus'] = $app->share(function($app) { 174 | $commandBus = new QueryBus(); 175 | $commandBus->register(new PostQueryHandler($app['post_view_repository'])); 176 | $commandBus->register(new AllPostsQueryHandler($app['post_view_repository'])); 177 | 178 | return $commandBus; 179 | }); 180 | 181 | return $app; 182 | -------------------------------------------------------------------------------- /src/console.php: -------------------------------------------------------------------------------- 1 | getDefinition()->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', 'dev')); 12 | $console->setDispatcher($app['dispatcher']); 13 | 14 | $console 15 | ->register('server:run') 16 | ->setDefinition([ 17 | new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', 'localhost:9000') 18 | ]) 19 | ->setDescription('Runs with PHP built-in web server') 20 | ->setHelp(<<%command.name% command runs the embedded web server: 22 | 23 | %command.full_name% 24 | 25 | You can also customize the default address and port the web server listens to: 26 | 27 | %command.full_name% 127.0.0.1:8080 28 | EOF 29 | ) 30 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($console) { 31 | 32 | if (version_compare(PHP_VERSION, '5.4.0') < 0) { 33 | throw new Exception('This feature only runs with PHP 5.4.0 or higher.'); 34 | } 35 | 36 | $app = __DIR__ . '/../web/index_dev.php'; 37 | while (!file_exists($app)) { 38 | $dialog = $console->getHelperSet()->get('dialog'); 39 | $app = $dialog->ask($output, sprintf('I cannot find "%s". What\'s the absoulte path of "console.php"? ', $app), __DIR__ . '/../web/index_dev.php'); 40 | } 41 | 42 | $output->writeln(sprintf('Application running on %s', $input->getArgument('address'))); 43 | 44 | $builder = new ProcessBuilder(array(PHP_BINARY, '-S', $input->getArgument('address'), $app)); 45 | 46 | $builder->setWorkingDirectory(getcwd()); 47 | $builder->setTimeout(null); 48 | $builder->getProcess()->run(function ($type, $buffer) use ($output) { 49 | if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) { 50 | $output->write($buffer); 51 | } 52 | }); 53 | }) 54 | ; 55 | 56 | return $console; 57 | -------------------------------------------------------------------------------- /src/controllers.php: -------------------------------------------------------------------------------- 1 | get( 14 | '/', 15 | function () use ($app) 16 | { 17 | $allPostsQuery = new AllPostsQuery(); 18 | $posts = $app['query_bus']->handle($allPostsQuery); 19 | 20 | return $app['twig']->render('index.html.twig', array('posts' => $posts)); 21 | } 22 | ) 23 | ->bind('homepage') 24 | ; 25 | 26 | $app 27 | ->get( 28 | '/post/new', 29 | function () use ($app) 30 | { 31 | $aPostCommand = new CreatePostCommand( 32 | 'Write a blog post', 33 | 'The Post title' 34 | ); 35 | 36 | $form = $app['form.factory']->createBuilder('form', $aPostCommand) 37 | ->add('title', 'text') 38 | ->add('content', 'textarea') 39 | ->add('save', 'submit') 40 | ->getForm() 41 | ; 42 | 43 | return $app['twig']->render('new_post.html.twig', ['form' => $form->createView()]); 44 | } 45 | ) 46 | ->bind('new_post') 47 | ; 48 | 49 | $app 50 | ->post( 51 | '/post/create', 52 | function (Request $request) use ($app) 53 | { 54 | $data = [ 55 | 'title' => 'The Post title', 56 | 'content' => 'Write a blog post' 57 | ]; 58 | 59 | $form = $app['form.factory']->createBuilder('form', $data) 60 | ->add('title', 'text') 61 | ->add('content', 'textarea') 62 | ->add('save', 'submit') 63 | ->getForm() 64 | ; 65 | 66 | $form->handleRequest($request); 67 | 68 | if ($form->isValid()) { 69 | $data = $form->getData(); 70 | 71 | $aPostCommand = new CreatePostCommand( 72 | $data['content'], 73 | $data['title'] 74 | ); 75 | 76 | $app['command_bus']->handle($aPostCommand); 77 | 78 | $app['session']->getFlashBag()->add('notices', 'Post was created!'); 79 | return $app->redirect($app['url_generator']->generate('homepage')); 80 | } 81 | 82 | return $app['twig']->render('new_post.html.twig', ['form' => $form->createView()]); 83 | } 84 | ) 85 | ->bind('create_post') 86 | ; 87 | 88 | $app 89 | ->get( 90 | '/post/show/{id}', 91 | function ($id) use ($app) 92 | { 93 | $postQuery = new PostQuery($id); 94 | $post = $app['query_bus']->handle($postQuery); 95 | 96 | $form = $app['form.factory']->createBuilder('form', ['comment' => 'Write a comment']) 97 | ->add('comment', 'textarea') 98 | ->add('save', 'submit') 99 | ->getForm() 100 | ; 101 | 102 | return $app['twig']->render('post.html.twig', ['post' => $post, 'form' => $form->createView()]); 103 | } 104 | ) 105 | ->bind('post') 106 | ; 107 | 108 | $app 109 | ->get( 110 | '/post/publish/{id}', 111 | function ($id) use ($app) 112 | { 113 | $app['command_bus']->handle(new PublishPostCommand($id)); 114 | 115 | $app['session']->getFlashBag()->add('notices', 'Post was published!'); 116 | return $app->redirect($app['url_generator']->generate('post', ['id' => $id])); 117 | } 118 | ) 119 | ; 120 | 121 | $app 122 | ->get( 123 | '/post/update/{id}', 124 | function ($id) use ($app) 125 | { 126 | $title = 'This is an update test for the title'; 127 | $content = 'This is an update test for the content'; 128 | 129 | $app['command_bus']->handle(new UpdatePostCommand($id, $content, $title)); 130 | 131 | $app['session']->getFlashBag()->add('notices', 'Post was updated!'); 132 | return $app->redirect($app['url_generator']->generate('post', ['id' => $id])); 133 | } 134 | ) 135 | ; 136 | 137 | $app 138 | ->post( 139 | '/post/{id}/comment', 140 | function ($id, Request $request) use ($app) 141 | { 142 | $data = [ 143 | 'comment' => 'Write a comment' 144 | ]; 145 | 146 | $form = $app['form.factory']->createBuilder('form', $data) 147 | ->add('comment', 'textarea') 148 | ->add('save', 'submit') 149 | ->getForm() 150 | ; 151 | 152 | $form->handleRequest($request); 153 | 154 | if ($form->isValid()) { 155 | $data = $form->getData(); 156 | 157 | $aCommentCommand = new CommentCommand( 158 | $id, 159 | $data['comment'] 160 | ); 161 | 162 | $app['command_bus']->handle($aCommentCommand); 163 | 164 | $app['session']->getFlashBag()->add('notices', 'Comment was added!'); 165 | return $app->redirect($app['url_generator']->generate('post', ['id' => $id])); 166 | } 167 | 168 | $postQuery = new PostQuery($id); 169 | $post = $app['query_bus']->handle($postQuery); 170 | 171 | return $app['twig']->render('post.html.twig', ['post' => $post, 'form' => $form->createView()]); 172 | } 173 | ) 174 | ->bind('comment') 175 | ; 176 | 177 | $app->error(function (Exception $e, $code) use ($app) { 178 | if ($app['debug']) { 179 | return; 180 | } 181 | 182 | // 404.html, or 40x.html, or 4xx.html, or error.html 183 | $templates = array( 184 | 'errors/'.$code.'.html', 185 | 'errors/'.substr($code, 0, 2).'x.html', 186 | 'errors/'.substr($code, 0, 1).'xx.html', 187 | 'errors/default.html', 188 | ); 189 | 190 | return new Response($app['twig']->resolveTemplate($templates)->render(array('code' => $code)), $code); 191 | }); 192 | -------------------------------------------------------------------------------- /templates/bootstrap.html.twig: -------------------------------------------------------------------------------- 1 | {% use "form_div_layout.html.twig" %} 2 | 3 | {# Widgets #} 4 | 5 | {% block form_widget %} 6 | {% spaceless %} 7 | {% if compound %} 8 | {{ block('form_widget_compound') }} 9 | {% else %} 10 | {{ block('form_widget_simple') }} 11 | {% endif %} 12 | {% endspaceless %} 13 | {% endblock form_widget %} 14 | 15 | {% block form_widget_simple %} 16 | {% spaceless %} 17 | {% set style = style|default(bootstrap_get_style()) %} 18 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 19 | 20 | {% if simple_col is not defined and bootstrap_get_simple_col() %} 21 | {% set simple_col = bootstrap_get_simple_col() %} 22 | {% endif %} 23 | {% if attr.simple_col is defined and attr.simple_col is not empty %} 24 | {% set simple_col = attr.simple_col %} 25 | {% endif %} 26 | {% if attr.col_size is defined and attr.col_size is not empty %} 27 | {% set col_size = attr.col_size %} 28 | {% endif %} 29 | {% if attr.style is defined and attr.style is not empty %} 30 | {% set style = attr.style %} 31 | {% endif %} 32 | 33 | {% if simple_col is defined and simple_col %} 34 |
35 | {% endif %} 36 | 37 | {% set type = type|default('text') %} 38 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' form-control')|trim }) %} 39 | 40 | {% if style == 'inline' and (attr.placeholder is not defined or attr.placeholder is empty) %} 41 | {% if label is empty %} 42 | {% set attr = attr|merge({ 'placeholder': name|humanize }) %} 43 | {% else %} 44 | {% set attr = attr|merge({ 'placeholder': label}) %} 45 | {% endif %} 46 | {% endif %} 47 | 48 | 49 | 50 | {% if simple_col is defined %} 51 |
52 | {% endif %} 53 | {% endspaceless %} 54 | {% endblock form_widget_simple %} 55 | 56 | {% block form_widget_compound %} 57 | {% spaceless %} 58 |
59 | {% if form.parent is empty %} 60 | {{ block('global_form_errors') }} 61 | {% endif %} 62 | {{ block('form_rows') }} 63 | {{ form_rest(form) }} 64 |
65 | {% endspaceless %} 66 | {% endblock form_widget_compound %} 67 | 68 | {% block collection_widget %} 69 | {% spaceless %} 70 | {% if prototype is defined %} 71 | {% set attr = attr|merge({'data-prototype': form_row(prototype) }) %} 72 | {% endif %} 73 | {{ block('form_widget') }} 74 | {% endspaceless %} 75 | {% endblock collection_widget %} 76 | 77 | {% block bootstrap_collection_widget %} 78 | {% spaceless %} 79 | {% if prototype is defined %} 80 | {% set prototype_vars = {} %} 81 | {% if style is defined %} 82 | {% set prototype_vars = prototype_vars|merge({'style': style}) %} 83 | {% endif %} 84 | {% set prototype_html = '
' ~ form_widget(prototype, prototype_vars) ~ '
' %} 85 | {% if form.vars.allow_delete %} 86 | {% set prototype_html = prototype_html ~ '
' ~ form.vars.delete_button_text|trans({}, translation_domain) ~ '
' %} 87 | {% endif %} 88 | {% set prototype_html = '
' ~ prototype_html ~ '
' %} 89 | 90 | {% set attr = attr|merge({'data-prototype': prototype_html }) %} 91 | {% set attr = attr|merge({'data-prototype-name': prototype_name }) %} 92 | {% endif %} 93 |
94 | 111 | {% if form.vars.allow_add %} 112 | {{ form.vars.add_button_text|trans({}, translation_domain) }} 113 | {% endif %} 114 |
115 | {% endspaceless %} 116 | {% endblock bootstrap_collection_widget %} 117 | 118 | {% block textarea_widget %} 119 | {% spaceless %} 120 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 121 | 122 | {% if attr.simple_col is defined and attr.simple_col is not empty %} 123 | {% set simple_col = attr.simple_col %} 124 | {% endif %} 125 | {% if attr.col_size is defined and attr.col_size is not empty %} 126 | {% set col_size = attr.col_size %} 127 | {% endif %} 128 | 129 | {% if simple_col is defined %} 130 |
131 | {% endif %} 132 | 133 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' form-control')|trim }) %} 134 | 135 | 136 | 137 | {% if simple_col is defined %} 138 |
139 | {% endif %} 140 | {% endspaceless %} 141 | {% endblock textarea_widget %} 142 | 143 | {% block file_widget %} 144 | {% spaceless %} 145 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 146 | 147 | {% if attr.simple_col is defined and attr.simple_col is not empty %} 148 | {% set simple_col = attr.simple_col %} 149 | {% endif %} 150 | 151 | {% if attr.col_size is defined and attr.col_size is not empty %} 152 | {% set col_size = attr.col_size %} 153 | {% endif %} 154 | 155 | {% if simple_col is defined %} 156 |
157 | {% endif %} 158 | 159 | 160 | 161 | {% if simple_col is defined %} 162 |
163 | {% endif %} 164 | {% endspaceless %} 165 | {% endblock file_widget %} 166 | 167 | {% block choice_widget %} 168 | {% spaceless %} 169 | {% if expanded %} 170 | {{ block('choice_widget_expanded') }} 171 | {% else %} 172 | {{ block('choice_widget_collapsed') }} 173 | {% endif %} 174 | {% endspaceless %} 175 | {% endblock choice_widget %} 176 | 177 | {% block choice_widget_expanded %} 178 | {% spaceless %} 179 |
180 | {% for child in form %} 181 | {% if form.multiple is defined %} 182 | {{ checkbox_row(child, { 'no_form_group': true }) }} 183 | {% else %} 184 | {{ radio_row(child, { 'no_form_group': true }) }} 185 | {% endif %} 186 | {% endfor %} 187 |
188 | {% endspaceless %} 189 | {% endblock choice_widget_expanded %} 190 | 191 | {% block choice_widget_collapsed %} 192 | {% spaceless %} 193 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' form-control')|trim }) %} 194 | 195 | 209 | {% endspaceless %} 210 | {% endblock choice_widget_collapsed %} 211 | 212 | {% block choice_widget_options %} 213 | {% spaceless %} 214 | {% for group_label, choice in options %} 215 | {% if choice is iterable %} 216 | 217 | {% set options = choice %} 218 | {{ block('choice_widget_options') }} 219 | 220 | {% else %} 221 | 222 | {% endif %} 223 | {% endfor %} 224 | {% endspaceless %} 225 | {% endblock choice_widget_options %} 226 | 227 | {% block checkbox_row %} 228 | {% spaceless %} 229 | {% set style = style|default(bootstrap_get_style()) %} 230 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 231 | 232 | {% if attr.label_col is defined and attr.label_col is not empty %} 233 | {% set label_col = attr.label_col %} 234 | {% endif %} 235 | {% if attr.widget_col is defined and attr.widget_col is not empty %} 236 | {% set widget_col = attr.widget_col %} 237 | {% endif %} 238 | {% if attr.col_size is defined and attr.col_size is not empty %} 239 | {% set col_size = attr.col_size %} 240 | {% endif %} 241 | {% if attr.style is defined and attr.style is not empty %} 242 | {% set style = attr.style %} 243 | {% endif %} 244 | 245 | {% set class = '' %} 246 | {% if align_with_widget is defined or attr.align_with_widget is defined %} 247 | {% set widget_col = widget_col|default(bootstrap_get_widget_col()) %} 248 | {% set label_col = label_col|default(bootstrap_get_label_col()) %} 249 | {% set class = 'col-' ~ col_size ~ '-' ~ widget_col ~ ' col-' ~ col_size ~ '-offset-' ~ label_col %} 250 |
251 |
252 | {% elseif no_form_group is not defined or no_form_group == false %} 253 |
254 | {% endif %} 255 |
256 | {% if label is not sameas(false) %} 257 | {% if not compound %} 258 | {% set label_attr = label_attr|merge({'for': id}) %} 259 | {% endif %} 260 | {% if required %} 261 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 262 | {% endif %} 263 | {% if label is empty %} 264 | {% set label = name|humanize %} 265 | {% endif %} 266 | 267 | {{ block('checkbox_widget') }} 268 | {{ label|trans({}, translation_domain) }} 269 | 270 | {% else %} 271 | {{ block('checkbox_widget') }} 272 | {% endif %} 273 | {{ form_errors(form) }} 274 |
275 | {% if align_with_widget is defined or attr.align_with_widget is defined %} 276 |
277 |
278 | {% elseif no_form_group is not defined or no_form_group == false %} 279 |
280 | {% endif %} 281 | 282 | {% if style == 'inline' %} {% endif %} 283 | {% endspaceless %} 284 | {% endblock checkbox_row %} 285 | 286 | {% block radio_row %} 287 | {% spaceless %} 288 | {% set class = '' %} 289 | 290 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 291 | 292 | {% if attr.label_col is defined and attr.label_col is not empty %} 293 | {% set label_col = attr.label_col %} 294 | {% endif %} 295 | {% if attr.widget_col is defined and attr.widget_col is not empty %} 296 | {% set widget_col = attr.widget_col %} 297 | {% endif %} 298 | {% if attr.col_size is defined and attr.col_size is not empty %} 299 | {% set col_size = attr.col_size %} 300 | {% endif %} 301 | 302 | {% if align_with_widget is defined or attr.align_with_widget is defined %} 303 | {% set widget_col = widget_col|default(bootstrap_get_widget_col()) %} 304 | {% set label_col = label_col|default(bootstrap_get_label_col()) %} 305 | {% set class = ' col-'~ col_size ~ '-' ~ widget_col ~ ' col-' ~ col_size ~ '-offset-' ~ label_col %} 306 |
307 |
308 | {% elseif no_form_group is not defined or no_form_group == false %} 309 |
310 | {% endif %} 311 | 312 |
313 | {% if label is not sameas(false) %} 314 | {% if not compound %} 315 | {% set label_attr = label_attr|merge({'for': id}) %} 316 | {% endif %} 317 | {% if required %} 318 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 319 | {% endif %} 320 | {% if label is empty %} 321 | {% set label = name|humanize %} 322 | {% endif %} 323 | 324 | {{ block('radio_widget') }} 325 | {{ label|trans({}, translation_domain) }} 326 | 327 | {% else %} 328 | {{ block('radio_widget') }} 329 | {% endif %} 330 | {{ form_errors(form) }} 331 |
332 | {% if align_with_widget is defined or attr.align_with_widget is defined %} 333 |
334 |
335 | {% elseif no_form_group is not defined or no_form_group == false %} 336 |
337 | {% endif %} 338 | {% endspaceless %} 339 | {% endblock radio_row %} 340 | 341 | {% block checkbox_widget %} 342 | {% spaceless %} 343 | 344 | {% endspaceless %} 345 | {% endblock checkbox_widget %} 346 | 347 | {% block radio_widget %} 348 | {% spaceless %} 349 | 350 | {% endspaceless %} 351 | {% endblock radio_widget %} 352 | 353 | {% block datetime_widget %} 354 | {% spaceless %} 355 | {% if widget == 'single_text' %} 356 | {{ block('form_widget_simple') }} 357 | {% else %} 358 | {% set attr = attr|merge({ 'class': 'bootstrap-datetime' }) %} 359 |
360 | {{ form_widget(form.date) }} 361 | {{ form_widget(form.time) }} 362 | {{ form_errors(form.date) }} 363 | {{ form_errors(form.time) }} 364 |
365 | {% endif %} 366 | {% endspaceless %} 367 | {% endblock datetime_widget %} 368 | 369 | {% block date_widget %} 370 | {% spaceless %} 371 | {% if widget == 'single_text' %} 372 | {{ block('form_widget_simple') }} 373 | {% else %} 374 | {% set attr = attr|merge({ 'class': 'bootstrap-date' }) %} 375 |
376 | {{ date_pattern|replace({ 377 | '{{ year }}': form_widget(form.year), 378 | '{{ month }}': form_widget(form.month), 379 | '{{ day }}': form_widget(form.day), 380 | })|raw }} 381 |
382 | {% endif %} 383 | {% endspaceless %} 384 | {% endblock date_widget %} 385 | 386 | {% block time_widget %} 387 | {% spaceless %} 388 | {% if widget == 'single_text' %} 389 | {{ block('form_widget_simple') }} 390 | {% else %} 391 | {% set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} %} 392 | {% set attr = attr|merge({ 'class': 'bootstrap-time' }) %} 393 |
394 | {{ form_widget(form.hour, vars) }} 395 | {% if with_minutes %}:{{ form_widget(form.minute, vars) }}{% endif %} 396 | {% if with_seconds %}:{{ form_widget(form.second, vars) }}{% endif %} 397 |
398 | {% endif %} 399 | {% endspaceless %} 400 | {% endblock time_widget %} 401 | 402 | {% block number_widget %} 403 | {% spaceless %} 404 | {# type="number" doesn't work with floats #} 405 | {% set type = type|default('text') %} 406 | {{ block('form_widget_simple') }} 407 | {% endspaceless %} 408 | {% endblock number_widget %} 409 | 410 | {% block integer_widget %} 411 | {% spaceless %} 412 | {% set type = type|default('number') %} 413 | {{ block('form_widget_simple') }} 414 | {% endspaceless %} 415 | {% endblock integer_widget %} 416 | 417 | {% block money_widget %} 418 | {% spaceless %} 419 |
420 | {{ money_pattern|replace({ 421 | '{{ widget }}': block('form_widget_simple'), 422 | '{{ tag_start }}': '', 423 | '{{ tag_end }}': '' 424 | })|raw }} 425 |
426 | {% endspaceless %} 427 | {% endblock money_widget %} 428 | 429 | {% block url_widget %} 430 | {% spaceless %} 431 | {% set type = type|default('url') %} 432 | {{ block('form_widget_simple') }} 433 | {% endspaceless %} 434 | {% endblock url_widget %} 435 | 436 | {% block search_widget %} 437 | {% spaceless %} 438 | {% set type = type|default('search') %} 439 | {{ block('form_widget_simple') }} 440 | {% endspaceless %} 441 | {% endblock search_widget %} 442 | 443 | {% block percent_widget %} 444 | {% spaceless %} 445 | {% set type = type|default('text') %} 446 |
447 | {{ block('form_widget_simple') }} 448 | % 449 |
450 | {% endspaceless %} 451 | {% endblock percent_widget %} 452 | 453 | {% block password_widget %} 454 | {% spaceless %} 455 | {% set type = type|default('password') %} 456 | {{ block('form_widget_simple') }} 457 | {% endspaceless %} 458 | {% endblock password_widget %} 459 | 460 | {% block hidden_widget %} 461 | {% spaceless %} 462 | {% set type = type|default('hidden') %} 463 | {{ block('form_widget_simple') }} 464 | {% endspaceless %} 465 | {% endblock hidden_widget %} 466 | 467 | {% block email_widget %} 468 | {% spaceless %} 469 | {% set type = type|default('email') %} 470 | {{ block('form_widget_simple') }} 471 | {% endspaceless %} 472 | {% endblock email_widget %} 473 | 474 | {% block button_widget %} 475 | {% spaceless %} 476 | {% if label is empty %} 477 | {% set label = name|humanize %} 478 | {% endif %} 479 | {% if type is defined and type == 'submit' %} 480 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' btn btn-'~attr.type|default('primary'))|trim }) %} 481 | {% else %} 482 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' btn btn-'~attr.type|default('default'))|trim }) %} 483 | {% endif %} 484 | 485 | {% endspaceless %} 486 | {% endblock button_widget %} 487 | 488 | {% block submit_widget %} 489 | {% spaceless %} 490 | {% set type = type|default('submit') %} 491 | {{ block('button_widget') }} 492 | {% endspaceless %} 493 | {% endblock submit_widget %} 494 | 495 | {% block reset_widget %} 496 | {% spaceless %} 497 | {% set type = type|default('reset') %} 498 | {{ block('button_widget') }} 499 | {% endspaceless %} 500 | {% endblock reset_widget %} 501 | 502 | {% block form_actions_widget %} 503 | {% for button in form.children %} 504 | {{ form_widget(button) }}  {# this needs to be here due to https://github.com/twbs/bootstrap/issues/3245 #} 505 | {% endfor %} 506 | {% endblock %} 507 | 508 | {# Labels #} 509 | 510 | {% block form_label %} 511 | {% spaceless %} 512 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 513 | 514 | {% if attr.label_col is defined and attr.label_col is not empty %} 515 | {% set label_col = attr.label_col %} 516 | {% endif %} 517 | {% if attr.widget_col is defined and attr.widget_col is not empty %} 518 | {% set widget_col = attr.widget_col %} 519 | {% endif %} 520 | {% if attr.col_size is defined and attr.col_size is not empty %} 521 | {% set col_size = attr.col_size %} 522 | {% endif %} 523 | 524 | {% if label is not sameas(false) %} 525 | {% set style = style|default(bootstrap_get_style()) %} 526 | {% set label_col = label_col|default(bootstrap_get_label_col()) %} 527 | 528 | {% if attr.style is defined and attr.style is not empty %} 529 | {% set style = attr.style %} 530 | {% endif %} 531 | 532 | {% set label_attr = label_attr|merge({ 'class': (label_attr.class|default('') ~ ' control-label')|trim }) %} 533 | {% if style == 'horizontal' %} 534 | {% set label_attr = label_attr|merge({ 'class': (label_attr.class|default('') ~ ' col-' ~ col_size ~ '-' ~ label_col)|trim }) %} 535 | {% elseif style == 'inline' %} 536 | {% set label_attr = label_attr|merge({ 'class': (label_attr.class|default('') ~ ' sr-only')|trim }) %} 537 | {% endif %} 538 | 539 | {% if not compound %} 540 | {% set label_attr = label_attr|merge({'for': id}) %} 541 | {% endif %} 542 | {% if required %} 543 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 544 | {% endif %} 545 | {% if label is empty %} 546 | {% set label = name|humanize %} 547 | {% endif %} 548 | {{ label|trans({}, translation_domain) }} 549 | {% endif %} 550 | {% endspaceless %} 551 | {% endblock form_label %} 552 | 553 | {% block button_label %}{% endblock %} 554 | 555 | {# Rows #} 556 | 557 | {% block repeated_row %} 558 | {% spaceless %} 559 | {# 560 | No need to render the errors here, as all errors are mapped 561 | to the first child (see RepeatedTypeValidatorExtension). 562 | #} 563 | {{ block('form_rows') }} 564 | {% endspaceless %} 565 | {% endblock repeated_row %} 566 | 567 | {% block form_row %} 568 | {% spaceless %} 569 | {% set style = style|default(bootstrap_get_style()) %} 570 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 571 | 572 | {% if attr.label_col is defined and attr.label_col is not empty %} 573 | {% set label_col = attr.label_col %} 574 | {% endif %} 575 | {% if attr.widget_col is defined and attr.widget_col is not empty %} 576 | {% set widget_col = attr.widget_col %} 577 | {% endif %} 578 | {% if attr.col_size is defined and attr.col_size is not empty %} 579 | {% set col_size = attr.col_size %} 580 | {% endif %} 581 | {% if attr.style is defined and attr.style is not empty %} 582 | {% set style = attr.style %} 583 | {% endif %} 584 | 585 | {% set label_col = label_col|default(bootstrap_get_label_col()) %} 586 | {% set widget_col = widget_col|default(bootstrap_get_widget_col()) %} 587 | 588 |
589 | {% if style == 'horizontal' %} 590 | {{ form_label(form) }} 591 |
592 | {{ block('form_input_group') }} 593 | {{ block('form_help') }} 594 | {{ form_errors(form) }} 595 |
596 | {% else %} 597 | {{ form_label(form) }} 598 | {{ block('form_input_group') }} 599 | {{ block('form_help') }} 600 | {{ form_errors(form) }} 601 | {% endif %} 602 |
603 | 604 | {% if style == 'inline' %} {% endif %} 605 | {% endspaceless %} 606 | {% endblock form_row %} 607 | 608 | {% block form_input_group %} 609 | {% spaceless %} 610 | {% if attr.input_group is defined and attr.input_group is not empty %} 611 | {% set input_group = attr.input_group %} 612 | {% endif %} 613 | {% set input_group = input_group|default({}) %} 614 | {% if input_group is not empty %} 615 | {% set ig_size_class = '' %} 616 | {% if input_group.size is defined and input_group.size == 'large' %} 617 | {% set ig_size_class = ' input-group-lg' %} 618 | {% endif %} 619 | {% if input_group.size is defined and input_group.size == 'small' %} 620 | {% set ig_size_class = ' input-group-sm' %} 621 | {% endif %} 622 |
623 | {% if input_group.prepend is defined and input_group.prepend is not empty %} 624 | {{ input_group.prepend|raw|parse_icons }} 625 | {% endif %} 626 | {{ form_widget(form) }} 627 | {% if input_group.append is defined and input_group.append is not empty %} 628 | {{ input_group.append|raw|parse_icons }} 629 | {% endif %} 630 |
631 | {% else %} 632 | {{ form_widget(form) }} 633 | {% endif %} 634 | {% endspaceless %} 635 | {% endblock form_input_group %} 636 | 637 | {% block form_help %} 638 | {% spaceless %} 639 | {% if attr.help_text is defined and attr.help_text is not empty %} 640 | {% set help_text = attr.help_text %} 641 | {% endif %} 642 | {% set help_text = help_text|default('') %} 643 | {% if help_text is not empty %} 644 | {{ help_text|trans({}, translation_domain) }} 645 | {% endif %} 646 | {% endspaceless %} 647 | {% endblock form_help %} 648 | 649 | {% block button_row %} 650 | {% spaceless %} 651 | {% set style = style|default(bootstrap_get_style()) %} 652 | {% set col_size = col_size|default(bootstrap_get_col_size()) %} 653 | 654 | {% if attr.label_col is defined and attr.label_col is not empty %} 655 | {% set label_col = attr.label_col %} 656 | {% endif %} 657 | {% if attr.widget_col is defined and attr.widget_col is not empty %} 658 | {% set widget_col = attr.widget_col %} 659 | {% endif %} 660 | {% if attr.col_size is defined and attr.col_size is not empty %} 661 | {% set col_size = attr.col_size %} 662 | {% endif %} 663 | {% if attr.style is defined and attr.style is not empty %} 664 | {% set style = attr.style %} 665 | {% endif %} 666 | 667 | {% set label_col = label_col|default(bootstrap_get_label_col()) %} 668 | {% set widget_col = widget_col|default(bootstrap_get_widget_col()) %} 669 | 670 | {% if style == 'horizontal' %} 671 |
672 |
673 | {% endif %} 674 | 675 | {{ form_widget(form) }} 676 | 677 | {% if style == 'horizontal' %} 678 |
679 |
680 | {% endif %} 681 | {% endspaceless %} 682 | {% endblock button_row %} 683 | 684 | {% block hidden_row %} 685 | {{ form_widget(form) }} 686 | {% endblock hidden_row %} 687 | 688 | {% block form_actions_row %} 689 | {{ block('button_row') }} 690 | {% endblock %} 691 | 692 | {# Misc #} 693 | 694 | {% block form %} 695 | {% spaceless %} 696 | {{ form_start(form) }} 697 | {{ form_widget(form) }} 698 | {{ form_end(form) }} 699 | {% endspaceless %} 700 | {% endblock form %} 701 | 702 | {% block form_start %} 703 | {% spaceless %} 704 | {% set method = method|upper %} 705 | {% if method in ["GET", "POST"] %} 706 | {% set form_method = method %} 707 | {% else %} 708 | {% set form_method = "POST" %} 709 | {% endif %} 710 | 711 | {% if style is defined %} 712 | {% set attr = attr|merge({ 'class': (attr.class|default('') ~ ' form-' ~ style)|trim }) %} 713 | {{ bootstrap_set_style(style) }} 714 | {% endif %} 715 | 716 | {% if col_size is defined %} 717 | {{ bootstrap_set_col_size(col_size) }} 718 | {% endif %} 719 | 720 | {% if widget_col is defined %} 721 | {{ bootstrap_set_widget_col(widget_col) }} 722 | {% endif %} 723 | 724 | {% if label_col is defined %} 725 | {{ bootstrap_set_label_col(label_col) }} 726 | {% endif %} 727 | 728 | {% if simple_col is defined %} 729 | {{ bootstrap_set_simple_col(simple_col) }} 730 | {% endif %} 731 | 732 | {% if attr.role is not defined or attr.role is empty %} 733 | {% set attr = attr|merge({ 'role': 'form' }) %} 734 | {% endif %} 735 | 736 |
737 | {% if form_method != method %} 738 | 739 | {% endif %} 740 | {% endspaceless %} 741 | {% endblock form_start %} 742 | 743 | {% block form_end %} 744 | {% spaceless %} 745 | {% if not render_rest is defined or render_rest %} 746 | {{ form_rest(form) }} 747 | {% endif %} 748 |
749 | {% if bootstrap_get_style() %} 750 | {{ bootstrap_set_style('') }} 751 | {% endif %} 752 | {% if bootstrap_get_col_size() %} 753 | {{ bootstrap_set_col_size('lg') }} 754 | {% endif %} 755 | {% if bootstrap_get_widget_col() %} 756 | {{ bootstrap_set_widget_col(10) }} 757 | {% endif %} 758 | {% if bootstrap_get_label_col() %} 759 | {{ bootstrap_set_label_col(2) }} 760 | {% endif %} 761 | {% if bootstrap_get_simple_col() %} 762 | {{ bootstrap_set_simple_col(false) }} 763 | {% endif %} 764 | {% endspaceless %} 765 | {% endblock form_end %} 766 | 767 | {% block form_enctype %} 768 | {% spaceless %} 769 | {% if multipart %}enctype="multipart/form-data"{% endif %} 770 | {% endspaceless %} 771 | {% endblock form_enctype %} 772 | 773 | {% block global_form_errors %} 774 | {% if errors|length > 0 %} 775 | {% set global_errors = true %} 776 | {{ block('form_errors') }} 777 | {% endif %} 778 | {% endblock global_form_errors %} 779 | 780 | {% block form_errors %} 781 | {% spaceless %} 782 | {% set global_errors = global_errors|default(false) %} 783 | {% set style = style|default(bootstrap_get_style()) %} 784 | 785 | {% if attr.style is defined and attr.style is not empty %} 786 | {% set style = attr.style %} 787 | {% endif %} 788 | 789 | {% if errors|length > 0 %} 790 | {% if global_errors %} 791 |
792 | {% endif %} 793 | 794 | {% for error in errors %} 795 |
  • {{ error.message|trans(error.messageParameters, translation_domain) }}
  • 796 | {% endfor %} 797 | 798 | {% if global_errors == true %} 799 |
    800 | {% endif %} 801 | {% endif %} 802 | {% endspaceless %} 803 | {% endblock form_errors %} 804 | 805 | {% block form_rest %} 806 | {% spaceless %} 807 | {% for child in form %} 808 | {% if not child.rendered %} 809 | {{ form_row(child) }} 810 | {% endif %} 811 | {% endfor %} 812 | {% endspaceless %} 813 | {% endblock form_rest %} 814 | 815 | {# Support #} 816 | 817 | {% block form_rows %} 818 | {% spaceless %} 819 | {% for child in form %} 820 | {% set childAttr = {} %} 821 | {% if attr.col_size is defined %} 822 | {% set childAttr = childAttr|merge({ 'col_size': attr.col_size }) %} 823 | {% endif %} 824 | {% if attr.widget_col is defined %} 825 | {% set childAttr = childAttr|merge({ 'widget_col': attr.widget_col }) %} 826 | {% endif %} 827 | {% if attr.label_col is defined %} 828 | {% set childAttr = childAttr|merge({ 'label_col': attr.label_col }) %} 829 | {% endif %} 830 | {% if attr.simple_col is defined %} 831 | {% set childAttr = childAttr|merge({ 'simple_col': attr.simple_col }) %} 832 | {% endif %} 833 | {% if attr.style is defined %} 834 | {% set childAttr = childAttr|merge({ 'style': attr.style }) %} 835 | {% endif %} 836 | {{ form_row(child, childAttr) }} 837 | {% endfor %} 838 | {% endspaceless %} 839 | {% endblock form_rows %} 840 | 841 | {% block widget_attributes %} 842 | {% spaceless %} 843 | id="{{ id }}" name="{{ full_name }}"{% if read_only %} readonly="readonly"{% endif %}{% if disabled %} disabled="disabled"{% endif %}{% if required %} required="required"{% endif %}{% if max_length %} maxlength="{{ max_length }}"{% endif %}{% if pattern %} pattern="{{ pattern }}"{% endif %} 844 | {% for attrname, attrvalue in attr %}{% if attrname in ['placeholder', 'title'] %}{{ attrname }}="{{ attrvalue|trans({}, translation_domain) }}" {% elseif attrname in ['input_group'] %}{% else %}{{ attrname }}="{{ attrvalue }}" {% endif %}{% endfor %} 845 | {% endspaceless %} 846 | {% endblock widget_attributes %} 847 | 848 | {% block widget_container_attributes %} 849 | {% spaceless %} 850 | {% if attr.style is defined and (attr.style == 'inline' or attr.style == 'horizontal') %} 851 | {% set attr = attr|merge({ 'class': 'form-'~attr.style~attr.class|default('') }) %} 852 | {% set attr = attr|merge({ 'style': null }) %} 853 | {% endif %} 854 | {% if id is not empty %}id="{{ id }}" {% endif %} 855 | {% for attrname, attrvalue in attr %}{% if attrvalue is not null %}{{ attrname }}="{{ attrvalue }}" {% endif %}{% endfor %} 856 | {% endspaceless %} 857 | {% endblock widget_container_attributes %} 858 | 859 | {% block button_attributes %} 860 | {% spaceless %} 861 | id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif %} 862 | {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %} 863 | {% endspaceless %} 864 | {% endblock button_attributes %} 865 | -------------------------------------------------------------------------------- /templates/errors/404.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | Page not found. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/errors/4xx.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | An error occurred on the client. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/errors/500.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | Internal server error. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/errors/5xx.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | An error occurred on the server. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/errors/default.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | An error occurred. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block title 'List of blog posts' %} 4 | 5 | {% block content %} 6 | 9 | 14 | {% endblock %} -------------------------------------------------------------------------------- /templates/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title '' %} - CQRS + ES Blog Engine 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 34 | 35 |
    36 | {% for notice in app.session.getFlashBag.get('notices') %} 37 |
    38 | 39 | Good job! {{ notice }} 40 |
    41 | {% endfor %} 42 | 43 | {% block content %}{% endblock %} 44 |
    45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /templates/new_post.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block title 'Create a new blog post' %} 4 | 5 | {% block content %} 6 | 9 | {{ form(form, {'action': path('create_post'), 'method': 'POST', 'style': 'horizontal'}) }} 10 | {% endblock %} -------------------------------------------------------------------------------- /templates/post.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block title post.title %} 4 | 5 | {% block content %} 6 | 9 |

    {{ post.content }}

    10 | {% if post.comments|length > 0 %} 11 |

    Comments

    12 | 17 | {% endif %} 18 | 21 | {{ form(form, {'action': path('comment', { 'id': post.id }), 'method': 'POST', 'style': 'horizontal'}) }} 22 | {% endblock %} -------------------------------------------------------------------------------- /var/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | 4 | -------------------------------------------------------------------------------- /var/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /web/assets/css/blog.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | body { 6 | color: #555; 7 | } 8 | 9 | h1, .h1, 10 | h2, .h2, 11 | h3, .h3, 12 | h4, .h4, 13 | h5, .h5, 14 | h6, .h6 { 15 | margin-top: 0; 16 | color: #333; 17 | } 18 | 19 | 20 | /* 21 | * Override Bootstrap's default container. 22 | */ 23 | 24 | @media (min-width: 1200px) { 25 | .container { 26 | width: 970px; 27 | } 28 | } 29 | 30 | 31 | /* 32 | * Masthead for nav 33 | */ 34 | 35 | .blog-masthead { 36 | background-color: #428bca; 37 | box-shadow: inset 0 -2px 5px rgba(0,0,0,.1); 38 | } 39 | 40 | /* Nav links */ 41 | .blog-nav-item { 42 | position: relative; 43 | display: inline-block; 44 | padding: 10px; 45 | font-weight: 500; 46 | color: #cdddeb; 47 | } 48 | .blog-nav-item:hover, 49 | .blog-nav-item:focus { 50 | color: #fff; 51 | text-decoration: none; 52 | } 53 | 54 | /* Active state gets a caret at the bottom */ 55 | .blog-nav .active { 56 | color: #fff; 57 | } 58 | .blog-nav .active:after { 59 | position: absolute; 60 | bottom: 0; 61 | left: 50%; 62 | width: 0; 63 | height: 0; 64 | margin-left: -5px; 65 | vertical-align: middle; 66 | content: " "; 67 | border-right: 5px solid transparent; 68 | border-bottom: 5px solid; 69 | border-left: 5px solid transparent; 70 | } 71 | 72 | 73 | /* 74 | * Blog name and description 75 | */ 76 | 77 | .blog-header { 78 | padding-top: 20px; 79 | padding-bottom: 20px; 80 | } 81 | .blog-title { 82 | margin-top: 30px; 83 | margin-bottom: 0; 84 | font-size: 60px; 85 | font-weight: normal; 86 | } 87 | .blog-description { 88 | font-size: 20px; 89 | color: #999; 90 | } 91 | 92 | 93 | /* 94 | * Main column and sidebar layout 95 | */ 96 | 97 | .blog-main { 98 | font-size: 18px; 99 | line-height: 1.5; 100 | } 101 | 102 | /* Sidebar modules for boxing content */ 103 | .sidebar-module { 104 | padding: 15px; 105 | margin: 0 -15px 15px; 106 | } 107 | .sidebar-module-inset { 108 | padding: 15px; 109 | background-color: #f5f5f5; 110 | border-radius: 4px; 111 | } 112 | .sidebar-module-inset p:last-child, 113 | .sidebar-module-inset ul:last-child, 114 | .sidebar-module-inset ol:last-child { 115 | margin-bottom: 0; 116 | } 117 | 118 | 119 | 120 | /* Pagination */ 121 | .pager { 122 | margin-bottom: 60px; 123 | text-align: left; 124 | } 125 | .pager > li > a { 126 | width: 140px; 127 | padding: 10px 20px; 128 | text-align: center; 129 | border-radius: 30px; 130 | } 131 | 132 | 133 | /* 134 | * Blog posts 135 | */ 136 | 137 | .blog-post { 138 | margin-bottom: 60px; 139 | } 140 | .blog-post-title { 141 | margin-bottom: 5px; 142 | font-size: 40px; 143 | } 144 | .blog-post-meta { 145 | margin-bottom: 20px; 146 | color: #999; 147 | } 148 | 149 | 150 | /* 151 | * Footer 152 | */ 153 | 154 | .blog-footer { 155 | padding: 40px 0; 156 | color: #999; 157 | text-align: center; 158 | background-color: #f9f9f9; 159 | border-top: 1px solid #e5e5e5; 160 | } -------------------------------------------------------------------------------- /web/assets/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn:active, 33 | .btn.active { 34 | background-image: none; 35 | } 36 | .btn-default { 37 | text-shadow: 0 1px 0 #fff; 38 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 39 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 41 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 42 | background-repeat: repeat-x; 43 | border-color: #dbdbdb; 44 | border-color: #ccc; 45 | } 46 | .btn-default:hover, 47 | .btn-default:focus { 48 | background-color: #e0e0e0; 49 | background-position: 0 -15px; 50 | } 51 | .btn-default:active, 52 | .btn-default.active { 53 | background-color: #e0e0e0; 54 | border-color: #dbdbdb; 55 | } 56 | .btn-primary { 57 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 58 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 60 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 61 | background-repeat: repeat-x; 62 | border-color: #2b669a; 63 | } 64 | .btn-primary:hover, 65 | .btn-primary:focus { 66 | background-color: #2d6ca2; 67 | background-position: 0 -15px; 68 | } 69 | .btn-primary:active, 70 | .btn-primary.active { 71 | background-color: #2d6ca2; 72 | border-color: #2b669a; 73 | } 74 | .btn-success { 75 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 76 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #3e8f3e; 81 | } 82 | .btn-success:hover, 83 | .btn-success:focus { 84 | background-color: #419641; 85 | background-position: 0 -15px; 86 | } 87 | .btn-success:active, 88 | .btn-success.active { 89 | background-color: #419641; 90 | border-color: #3e8f3e; 91 | } 92 | .btn-info { 93 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 94 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 95 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 96 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 97 | background-repeat: repeat-x; 98 | border-color: #28a4c9; 99 | } 100 | .btn-info:hover, 101 | .btn-info:focus { 102 | background-color: #2aabd2; 103 | background-position: 0 -15px; 104 | } 105 | .btn-info:active, 106 | .btn-info.active { 107 | background-color: #2aabd2; 108 | border-color: #28a4c9; 109 | } 110 | .btn-warning { 111 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 112 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 113 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 114 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 115 | background-repeat: repeat-x; 116 | border-color: #e38d13; 117 | } 118 | .btn-warning:hover, 119 | .btn-warning:focus { 120 | background-color: #eb9316; 121 | background-position: 0 -15px; 122 | } 123 | .btn-warning:active, 124 | .btn-warning.active { 125 | background-color: #eb9316; 126 | border-color: #e38d13; 127 | } 128 | .btn-danger { 129 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 130 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 131 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 132 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 133 | background-repeat: repeat-x; 134 | border-color: #b92c28; 135 | } 136 | .btn-danger:hover, 137 | .btn-danger:focus { 138 | background-color: #c12e2a; 139 | background-position: 0 -15px; 140 | } 141 | .btn-danger:active, 142 | .btn-danger.active { 143 | background-color: #c12e2a; 144 | border-color: #b92c28; 145 | } 146 | .thumbnail, 147 | .img-thumbnail { 148 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 149 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 150 | } 151 | .dropdown-menu > li > a:hover, 152 | .dropdown-menu > li > a:focus { 153 | background-color: #e8e8e8; 154 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 155 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 156 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 157 | background-repeat: repeat-x; 158 | } 159 | .dropdown-menu > .active > a, 160 | .dropdown-menu > .active > a:hover, 161 | .dropdown-menu > .active > a:focus { 162 | background-color: #357ebd; 163 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 164 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 165 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 166 | background-repeat: repeat-x; 167 | } 168 | .navbar-default { 169 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 170 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 171 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 172 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 173 | background-repeat: repeat-x; 174 | border-radius: 4px; 175 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 176 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 177 | } 178 | .navbar-default .navbar-nav > .active > a { 179 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 180 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 182 | background-repeat: repeat-x; 183 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 184 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 185 | } 186 | .navbar-brand, 187 | .navbar-nav > li > a { 188 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 189 | } 190 | .navbar-inverse { 191 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 192 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 193 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 194 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 195 | background-repeat: repeat-x; 196 | } 197 | .navbar-inverse .navbar-nav > .active > a { 198 | background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); 199 | background-image: linear-gradient(to bottom, #222 0%, #282828 100%); 200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 201 | background-repeat: repeat-x; 202 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 203 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 204 | } 205 | .navbar-inverse .navbar-brand, 206 | .navbar-inverse .navbar-nav > li > a { 207 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 208 | } 209 | .navbar-static-top, 210 | .navbar-fixed-top, 211 | .navbar-fixed-bottom { 212 | border-radius: 0; 213 | } 214 | .alert { 215 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 216 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 217 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 218 | } 219 | .alert-success { 220 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 221 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 222 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 223 | background-repeat: repeat-x; 224 | border-color: #b2dba1; 225 | } 226 | .alert-info { 227 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 228 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 230 | background-repeat: repeat-x; 231 | border-color: #9acfea; 232 | } 233 | .alert-warning { 234 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 235 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 236 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 237 | background-repeat: repeat-x; 238 | border-color: #f5e79e; 239 | } 240 | .alert-danger { 241 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 242 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 243 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 244 | background-repeat: repeat-x; 245 | border-color: #dca7a7; 246 | } 247 | .progress { 248 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 249 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 250 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 251 | background-repeat: repeat-x; 252 | } 253 | .progress-bar { 254 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 255 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 257 | background-repeat: repeat-x; 258 | } 259 | .progress-bar-success { 260 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 261 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 262 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 263 | background-repeat: repeat-x; 264 | } 265 | .progress-bar-info { 266 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 267 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 268 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 269 | background-repeat: repeat-x; 270 | } 271 | .progress-bar-warning { 272 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 273 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 274 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 275 | background-repeat: repeat-x; 276 | } 277 | .progress-bar-danger { 278 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 279 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 281 | background-repeat: repeat-x; 282 | } 283 | .list-group { 284 | border-radius: 4px; 285 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 286 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 287 | } 288 | .list-group-item.active, 289 | .list-group-item.active:hover, 290 | .list-group-item.active:focus { 291 | text-shadow: 0 -1px 0 #3071a9; 292 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 293 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 295 | background-repeat: repeat-x; 296 | border-color: #3278b3; 297 | } 298 | .panel { 299 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .panel-default > .panel-heading { 303 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 304 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 305 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 306 | background-repeat: repeat-x; 307 | } 308 | .panel-primary > .panel-heading { 309 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 310 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 311 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 312 | background-repeat: repeat-x; 313 | } 314 | .panel-success > .panel-heading { 315 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 316 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 317 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 318 | background-repeat: repeat-x; 319 | } 320 | .panel-info > .panel-heading { 321 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 322 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 323 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 324 | background-repeat: repeat-x; 325 | } 326 | .panel-warning > .panel-heading { 327 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 328 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 329 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 330 | background-repeat: repeat-x; 331 | } 332 | .panel-danger > .panel-heading { 333 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 334 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .well { 339 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 340 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 341 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 342 | background-repeat: repeat-x; 343 | border-color: #dcdcdc; 344 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 345 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 346 | } 347 | /*# sourceMappingURL=bootstrap-theme.css.map */ 348 | -------------------------------------------------------------------------------- /web/assets/css/bootstrap-theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins.less"],"names":[],"mappings":"AAeA;AACA;AACA;AACA;AACA;AACA;EACE,wCAAA;ECoGA,2FAAA;EACQ,mFAAA;;ADhGR,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;AACD,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;EC8FD,wDAAA;EACQ,gDAAA;;ADnER,IAAC;AACD,IAAC;EACC,sBAAA;;AAKJ;EC4PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;EAyB2C,yBAAA;EAA2B,kBAAA;;AAvBtE,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAeJ;EC2PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAgBJ;EC0PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAiBJ;ECyPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,SAAC;AACD,SAAC;EACC,yBAAA;EACA,qBAAA;;AAkBJ;ECwPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAmBJ;ECuPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,WAAC;AACD,WAAC;EACC,yBAAA;EACA,qBAAA;;AA2BJ;AACA;EC6CE,kDAAA;EACQ,0CAAA;;ADpCV,cAAe,KAAK,IAAG;AACvB,cAAe,KAAK,IAAG;ECmOnB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EDpOF,yBAAA;;AAEF,cAAe,UAAU;AACzB,cAAe,UAAU,IAAG;AAC5B,cAAe,UAAU,IAAG;EC6NxB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED9NF,yBAAA;;AAUF;ECiNI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;EDrPA,kBAAA;ECaA,2FAAA;EACQ,mFAAA;;ADjBV,eAOE,YAAY,UAAU;EC0MpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,wDAAA;EACQ,gDAAA;;ADLV;AACA,WAAY,KAAK;EACf,8CAAA;;AAIF;EC+LI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;;ADtOF,eAIE,YAAY,UAAU;EC2LpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,uDAAA;EACQ,+CAAA;;ADCV,eASE;AATF,eAUE,YAAY,KAAK;EACf,yCAAA;;AAKJ;AACA;AACA;EACE,gBAAA;;AAUF;EACE,6CAAA;EChCA,0FAAA;EACQ,kFAAA;;AD2CV;ECqJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAKF;ECoJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAMF;ECmJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAOF;ECkJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAgBF;ECyII,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADlIJ;EC+HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADjIJ;EC8HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADhIJ;EC6HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD/HJ;EC4HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD9HJ;EC2HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtHJ;EACE,kBAAA;EC/EA,kDAAA;EACQ,0CAAA;;ADiFV,gBAAgB;AAChB,gBAAgB,OAAO;AACvB,gBAAgB,OAAO;EACrB,6BAAA;EC4GE,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED7GF,qBAAA;;AAUF;ECjGE,iDAAA;EACQ,yCAAA;;AD0GV,cAAe;ECsFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADxFJ,cAAe;ECqFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADvFJ,cAAe;ECoFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtFJ,WAAY;ECmFR,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADrFJ,cAAe;ECkFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADpFJ,aAAc;ECiFV,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD5EJ;ECyEI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED1EF,qBAAA;EC1HA,yFAAA;EACQ,iFAAA","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n\n// WebKit-style focus\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n width: @width;\n height: @height;\n}\n.square(@size) {\n .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support the\n// standard `box-shadow` property.\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n -webkit-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n -webkit-transform: scale(@ratio, @ratio-y);\n -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n -webkit-transform: skew(@x, @y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n resize: @direction; // Options: horizontal, vertical, both\n overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse .panel-body {\n border-top-color: @border;\n }\n }\n & > .panel-footer {\n + .panel-collapse .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a& {\n color: @color;\n\n .list-group-item-heading { color: inherit; }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:hover,\n &:focus,\n &:active,\n &.active,\n .open .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 8%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &:active,\n &.active {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n background-color: @color;\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n background-color: @color;\n .progress-striped & {\n #gradient > .striped();\n }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n @media (min-width: @screen-xs-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-push(@columns) {\n @media (min-width: @screen-xs-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-pull(@columns) {\n @media (min-width: @screen-xs-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n // Color the label and help text\n .help-block,\n .control-label,\n .radio,\n .checkbox,\n .radio-inline,\n .checkbox-inline {\n color: @text-color;\n }\n // Set the border and box shadow on specific inputs to match\n .form-control {\n border-color: @border-color;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n &:focus {\n border-color: darken(@border-color, 10%);\n @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n .box-shadow(@shadow);\n }\n }\n // Set validation states also for addons\n .input-group-addon {\n color: @text-color;\n border-color: @border-color;\n background-color: @background-color;\n }\n // Optional feedback icon\n .form-control-feedback {\n color: @text-color;\n }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n &:focus {\n border-color: @color;\n outline: 0;\n .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. `