├── .gitignore ├── Dockerfile ├── README.md ├── blog ├── .gitignore ├── app │ ├── .htaccess │ ├── AppCache.php │ ├── AppKernel.php │ ├── Resources │ │ └── views │ │ │ ├── base.html.twig │ │ │ └── default │ │ │ └── index.html.twig │ ├── SymfonyRequirements.php │ ├── autoload.php │ ├── cache │ │ └── .gitkeep │ ├── check.php │ ├── config │ │ ├── config.yml │ │ ├── config_dev.yml │ │ ├── config_prod.yml │ │ ├── config_test.yml │ │ ├── parameters.yml.dist │ │ ├── routing.yml │ │ ├── routing_dev.yml │ │ ├── security.yml │ │ └── services.yml │ ├── console │ ├── logs │ │ └── .gitkeep │ └── phpunit.xml.dist ├── behat.yml ├── composer.json ├── composer.lock ├── features │ ├── addingComment.feature │ ├── bootstrap │ │ └── DomainContext.php │ ├── listingPosts.feature │ ├── publishingPost.feature │ └── updatingPost.feature ├── spec │ └── Domain │ │ ├── Aggregate │ │ ├── AggregateId │ │ │ ├── CommentIdSpec.php │ │ │ └── PostIdSpec.php │ │ ├── CommentSpec.php │ │ └── PostSpec.php │ │ ├── Event │ │ ├── CommentWasAddedSpec.php │ │ ├── PostWasPublishedSpec.php │ │ └── PostWasUpdatedSpec.php │ │ ├── EventEngine │ │ └── EventBusSpec.php │ │ └── ReadModel │ │ ├── Listener │ │ └── PostListListenerSpec.php │ │ ├── Populator │ │ └── PostListPopulatorSpec.php │ │ └── Projection │ │ └── PostListProjectionSpec.php ├── src │ ├── .htaccess │ ├── AppBundle │ │ ├── AppBundle.php │ │ ├── Command │ │ │ ├── ClearProjectionCommand.php │ │ │ ├── LoadFixturesCommand.php │ │ │ ├── PopulateProjectionCommand.php │ │ │ └── ProjectionCommand.php │ │ ├── Controller │ │ │ └── BlogController.php │ │ ├── Entity │ │ │ ├── FromAggregate.php │ │ │ ├── FromProjection.php │ │ │ └── PostListItem.php │ │ └── Resources │ │ │ ├── config │ │ │ ├── routing.yml │ │ │ └── services.yml │ │ │ └── views │ │ │ └── Blog │ │ │ ├── base.html.twig │ │ │ └── list.html.twig │ ├── Domain │ │ ├── Aggregate │ │ │ ├── AggregateId │ │ │ │ ├── CommentId.php │ │ │ │ └── PostId.php │ │ │ ├── Comment.php │ │ │ └── Post.php │ │ ├── AggregateHistory │ │ │ ├── CommentAggregateHistory.php │ │ │ └── PostAggregateHistory.php │ │ ├── Event │ │ │ ├── CommentWasAdded.php │ │ │ ├── PostWasPublished.php │ │ │ └── PostWasUpdated.php │ │ ├── EventEngine │ │ │ ├── Aggregate.php │ │ │ ├── AggregateHistory.php │ │ │ ├── AggregateId.php │ │ │ ├── BaseAggregateHistory.php │ │ │ ├── DomainEvent.php │ │ │ ├── EventBus.php │ │ │ ├── EventSourced.php │ │ │ └── EventStorage.php │ │ ├── FixturesEngine │ │ │ ├── AbstractFixture.php │ │ │ ├── Data │ │ │ │ └── PostFixtures.php │ │ │ ├── EventStorageNotEmptyException.php │ │ │ ├── FixtureInterface.php │ │ │ ├── FixturesExecutor.php │ │ │ └── ReferenceRepository.php │ │ ├── ReadModel │ │ │ ├── AbstractDomainEventListener.php │ │ │ ├── BulkProjectionStorage.php │ │ │ ├── DomainEventListener.php │ │ │ ├── Listener │ │ │ │ └── PostListListener.php │ │ │ ├── Populator │ │ │ │ └── PostListPopulator.php │ │ │ ├── Projection.php │ │ │ ├── Projection │ │ │ │ └── PostListProjection.php │ │ │ ├── ProjectionPopulator.php │ │ │ ├── ProjectionPopulatorStats.php │ │ │ └── ProjectionStorage.php │ │ └── UseCase │ │ │ ├── AddComment.php │ │ │ ├── AddComment │ │ │ ├── Command.php │ │ │ └── Responder.php │ │ │ ├── ListPosts.php │ │ │ ├── ListPosts │ │ │ ├── Command.php │ │ │ └── Responder.php │ │ │ ├── PublishPost.php │ │ │ ├── PublishPost │ │ │ ├── Command.php │ │ │ └── Responder.php │ │ │ ├── UpdatePost.php │ │ │ └── UpdatePost │ │ │ ├── Command.php │ │ │ └── Responder.php │ └── Infrastructure │ │ ├── InMemory │ │ ├── Document │ │ │ └── StoredEvent.php │ │ ├── InMemoryEventStorage.php │ │ └── InMemoryProjectionStorage.php │ │ └── ODM │ │ ├── Document │ │ ├── StoredEvent.php │ │ └── StoredProjection.php │ │ ├── Mapping │ │ ├── StoredEvent.mongodb.yml │ │ └── StoredProjection.mongodb.yml │ │ ├── ODMBulkProjectionStorage.php │ │ ├── ODMEventStorage.php │ │ └── ODMProjectionStorage.php └── web │ ├── .htaccess │ ├── app.php │ ├── app_dev.php │ ├── apple-touch-icon.png │ ├── config.php │ ├── favicon.ico │ └── robots.txt ├── docker-compose.yml └── docker ├── php.ini └── vhost.conf /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | demo/app/bootstrap.php.cache 4 | demo/app/config/parameters.yml 5 | demo/app/phpunit.xml 6 | demo/app/cache 7 | demo/app/logs 8 | demo/bin/ 9 | !demo/bin/.gitkeep 10 | demo/vendor/ 11 | demo/web/bundles/ 12 | 13 | logs/nginx/* 14 | !logs/nginx/.gitkeep 15 | logs/symfony/* 16 | !logs/symfony/.gitkeep 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sskorc/symfony2-mongo:latest 2 | 3 | ADD . /var/www 4 | 5 | RUN chown -R www-data:www-data /var/www 6 | RUN chown -R www-data:www-data /tmp 7 | 8 | ADD docker/php.ini /usr/local/etc/php/php.ini 9 | 10 | WORKDIR /var/www/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpcon-demo 2 | 3 | Application dedicated to be a presentation complement. 4 | It provides a very simple Blog with just few basic functionalities. 5 | It was designed to simply present how an event sourced application works. 6 | 7 | 8 | ## Setting up environment 9 | 10 | PHPcon Demo environment is based on docker containers, 11 | so make sure your environment supports docker and the docker-machine is up. 12 | 13 | Then start the machines with command: 14 | 15 | ``` 16 | $ docker-compose up -d 17 | ``` 18 | 19 | Check if all containers started: 20 | 21 | ``` 22 | $ docker ps -a 23 | ``` 24 | 25 | The output should look similar to that like: 26 | 27 | Status should be "Up ..." on 4 containers: 28 | 29 | ``` 30 | phpcondemo_nginx_1 31 | phpcondemo_php_1 32 | phpcondemo_db_1 33 | ``` 34 | 35 | You can log into *php* machine: 36 | 37 | ``` 38 | $ docker exec -it phpcondemo_php_1 bash 39 | ``` 40 | 41 | Go to ```/var/www/blog``` directory and install application with composer: 42 | 43 | ``` 44 | $ composer install -n 45 | ``` 46 | 47 | ## Running tests 48 | 49 | ### Behat 50 | 51 | To run Behat scenarios login to *php* container, 52 | go to ```/var/www/blog``` directory and run: 53 | 54 | ``` 55 | $ bin/behat 56 | ``` 57 | 58 | ### PHPSpec 59 | 60 | To run PHPSpec and/or see classes specs login to *php* container, 61 | go to ```/var/www/blog``` directory and run: 62 | 63 | ``` 64 | $ bin/phpspec run -fpretty 65 | ``` 66 | 67 | ## Application commands 68 | 69 | When logged into *php* container, you can run one of created commandline tools: 70 | 71 | ### Loading fixtures 72 | 73 | ``` 74 | $ php app/console ulff:fixtures:load 75 | ``` 76 | 77 | This command fills event storage and projection storage with some sample data. 78 | Command looks similar to the one avaiable with *DoctrineFixturesBundle* but operates 79 | on PHPCon Demo use cases. 80 | 81 | ### Cleaning projection 82 | 83 | ``` 84 | $ php app/console ulff:projection:clear --name=projection-name 85 | ``` 86 | 87 | This command removes all data from projection defined in parameter *name*. 88 | Just fill in correct projection name, e.g. *post-list*. 89 | 90 | ### Replaying projection 91 | 92 | ``` 93 | $ php app/console ulff:projection:populate --name=projection-name 94 | ``` 95 | 96 | This command cleans and replays projection defined in parameter *name*. 97 | Just fill in correct projection name, e.g. *post-list*. Your projection data will be then 98 | recreated from event storage. 99 | 100 | 101 | ## Browsing MongoDB data 102 | 103 | Log into *db* machine: 104 | 105 | ``` 106 | $ docker exec -it phpcondemo_db_1 bash 107 | ``` 108 | 109 | Then run MongoDB: 110 | 111 | ``` 112 | $ mongo 113 | ``` 114 | 115 | And switch to ```phpcondemo``` database: 116 | 117 | ``` 118 | > use phpcondemo 119 | ``` 120 | -------------------------------------------------------------------------------- /blog/.gitignore: -------------------------------------------------------------------------------- 1 | /app/bootstrap.php.cache 2 | /app/cache/* 3 | !app/cache/.gitkeep 4 | /app/config/parameters.yml 5 | /app/logs/* 6 | !app/logs/.gitkeep 7 | /app/phpunit.xml 8 | /bin/ 9 | /composer.phar 10 | /vendor/ 11 | /web/bundles/ 12 | -------------------------------------------------------------------------------- /blog/app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /blog/app/AppCache.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), array('dev', 'test'))) { 24 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); 25 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 26 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); 27 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); 28 | } 29 | 30 | return $bundles; 31 | } 32 | 33 | public function registerContainerConfiguration(LoaderInterface $loader) 34 | { 35 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); 36 | } 37 | 38 | public function getCacheDir() 39 | { 40 | return '/tmp/sf2/cache/' . $this->environment; 41 | } 42 | 43 | public function getLogDir() 44 | { 45 | return '/tmp/sf2/logs'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blog/app/Resources/views/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | 10 | {% block body %}{% endblock %} 11 | {% block javascripts %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /blog/app/Resources/views/default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |

Welcome to Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}

8 |
9 | 10 |
11 |

12 | 13 | 14 | Your application is ready to start working on it at: 15 | {{ base_dir }}/ 16 |

17 |
18 | 19 | 44 | 45 |
46 |
47 | {% endblock %} 48 | 49 | {% block stylesheets %} 50 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /blog/app/SymfonyRequirements.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /* 13 | * Users of PHP 5.2 should be able to run the requirements checks. 14 | * This is why the file and all classes must be compatible with PHP 5.2+ 15 | * (e.g. not using namespaces and closures). 16 | * 17 | * ************** CAUTION ************** 18 | * 19 | * DO NOT EDIT THIS FILE as it will be overridden by Composer as part of 20 | * the installation/update process. The original file resides in the 21 | * SensioDistributionBundle. 22 | * 23 | * ************** CAUTION ************** 24 | */ 25 | 26 | /** 27 | * Represents a single PHP requirement, e.g. an installed extension. 28 | * It can be a mandatory requirement or an optional recommendation. 29 | * There is a special subclass, named PhpIniRequirement, to check a php.ini configuration. 30 | * 31 | * @author Tobias Schultze 32 | */ 33 | class Requirement 34 | { 35 | private $fulfilled; 36 | private $testMessage; 37 | private $helpText; 38 | private $helpHtml; 39 | private $optional; 40 | 41 | /** 42 | * Constructor that initializes the requirement. 43 | * 44 | * @param bool $fulfilled Whether the requirement is fulfilled 45 | * @param string $testMessage The message for testing the requirement 46 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 47 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 48 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 49 | */ 50 | public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) 51 | { 52 | $this->fulfilled = (bool) $fulfilled; 53 | $this->testMessage = (string) $testMessage; 54 | $this->helpHtml = (string) $helpHtml; 55 | $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText; 56 | $this->optional = (bool) $optional; 57 | } 58 | 59 | /** 60 | * Returns whether the requirement is fulfilled. 61 | * 62 | * @return bool true if fulfilled, otherwise false 63 | */ 64 | public function isFulfilled() 65 | { 66 | return $this->fulfilled; 67 | } 68 | 69 | /** 70 | * Returns the message for testing the requirement. 71 | * 72 | * @return string The test message 73 | */ 74 | public function getTestMessage() 75 | { 76 | return $this->testMessage; 77 | } 78 | 79 | /** 80 | * Returns the help text for resolving the problem. 81 | * 82 | * @return string The help text 83 | */ 84 | public function getHelpText() 85 | { 86 | return $this->helpText; 87 | } 88 | 89 | /** 90 | * Returns the help text formatted in HTML. 91 | * 92 | * @return string The HTML help 93 | */ 94 | public function getHelpHtml() 95 | { 96 | return $this->helpHtml; 97 | } 98 | 99 | /** 100 | * Returns whether this is only an optional recommendation and not a mandatory requirement. 101 | * 102 | * @return bool true if optional, false if mandatory 103 | */ 104 | public function isOptional() 105 | { 106 | return $this->optional; 107 | } 108 | } 109 | 110 | /** 111 | * Represents a PHP requirement in form of a php.ini configuration. 112 | * 113 | * @author Tobias Schultze 114 | */ 115 | class PhpIniRequirement extends Requirement 116 | { 117 | /** 118 | * Constructor that initializes the requirement. 119 | * 120 | * @param string $cfgName The configuration name used for ini_get() 121 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 122 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 123 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 124 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 125 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 126 | * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 127 | * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 128 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 129 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 130 | */ 131 | public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) 132 | { 133 | $cfgValue = ini_get($cfgName); 134 | 135 | if (is_callable($evaluation)) { 136 | if (null === $testMessage || null === $helpHtml) { 137 | throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.'); 138 | } 139 | 140 | $fulfilled = call_user_func($evaluation, $cfgValue); 141 | } else { 142 | if (null === $testMessage) { 143 | $testMessage = sprintf('%s %s be %s in php.ini', 144 | $cfgName, 145 | $optional ? 'should' : 'must', 146 | $evaluation ? 'enabled' : 'disabled' 147 | ); 148 | } 149 | 150 | if (null === $helpHtml) { 151 | $helpHtml = sprintf('Set %s to %s in php.ini*.', 152 | $cfgName, 153 | $evaluation ? 'on' : 'off' 154 | ); 155 | } 156 | 157 | $fulfilled = $evaluation == $cfgValue; 158 | } 159 | 160 | parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); 161 | } 162 | } 163 | 164 | /** 165 | * A RequirementCollection represents a set of Requirement instances. 166 | * 167 | * @author Tobias Schultze 168 | */ 169 | class RequirementCollection implements IteratorAggregate 170 | { 171 | private $requirements = array(); 172 | 173 | /** 174 | * Gets the current RequirementCollection as an Iterator. 175 | * 176 | * @return Traversable A Traversable interface 177 | */ 178 | public function getIterator() 179 | { 180 | return new ArrayIterator($this->requirements); 181 | } 182 | 183 | /** 184 | * Adds a Requirement. 185 | * 186 | * @param Requirement $requirement A Requirement instance 187 | */ 188 | public function add(Requirement $requirement) 189 | { 190 | $this->requirements[] = $requirement; 191 | } 192 | 193 | /** 194 | * Adds a mandatory requirement. 195 | * 196 | * @param bool $fulfilled Whether the requirement is fulfilled 197 | * @param string $testMessage The message for testing the requirement 198 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 199 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 200 | */ 201 | public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null) 202 | { 203 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false)); 204 | } 205 | 206 | /** 207 | * Adds an optional recommendation. 208 | * 209 | * @param bool $fulfilled Whether the recommendation is fulfilled 210 | * @param string $testMessage The message for testing the recommendation 211 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 212 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 213 | */ 214 | public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null) 215 | { 216 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true)); 217 | } 218 | 219 | /** 220 | * Adds a mandatory requirement in form of a php.ini configuration. 221 | * 222 | * @param string $cfgName The configuration name used for ini_get() 223 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 224 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 225 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 226 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 227 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 228 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 229 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 230 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 231 | */ 232 | public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 233 | { 234 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); 235 | } 236 | 237 | /** 238 | * Adds an optional recommendation in form of a php.ini configuration. 239 | * 240 | * @param string $cfgName The configuration name used for ini_get() 241 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 242 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 243 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 244 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 245 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 246 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 247 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 248 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 249 | */ 250 | public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 251 | { 252 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); 253 | } 254 | 255 | /** 256 | * Adds a requirement collection to the current set of requirements. 257 | * 258 | * @param RequirementCollection $collection A RequirementCollection instance 259 | */ 260 | public function addCollection(RequirementCollection $collection) 261 | { 262 | $this->requirements = array_merge($this->requirements, $collection->all()); 263 | } 264 | 265 | /** 266 | * Returns both requirements and recommendations. 267 | * 268 | * @return array Array of Requirement instances 269 | */ 270 | public function all() 271 | { 272 | return $this->requirements; 273 | } 274 | 275 | /** 276 | * Returns all mandatory requirements. 277 | * 278 | * @return array Array of Requirement instances 279 | */ 280 | public function getRequirements() 281 | { 282 | $array = array(); 283 | foreach ($this->requirements as $req) { 284 | if (!$req->isOptional()) { 285 | $array[] = $req; 286 | } 287 | } 288 | 289 | return $array; 290 | } 291 | 292 | /** 293 | * Returns the mandatory requirements that were not met. 294 | * 295 | * @return array Array of Requirement instances 296 | */ 297 | public function getFailedRequirements() 298 | { 299 | $array = array(); 300 | foreach ($this->requirements as $req) { 301 | if (!$req->isFulfilled() && !$req->isOptional()) { 302 | $array[] = $req; 303 | } 304 | } 305 | 306 | return $array; 307 | } 308 | 309 | /** 310 | * Returns all optional recommendations. 311 | * 312 | * @return array Array of Requirement instances 313 | */ 314 | public function getRecommendations() 315 | { 316 | $array = array(); 317 | foreach ($this->requirements as $req) { 318 | if ($req->isOptional()) { 319 | $array[] = $req; 320 | } 321 | } 322 | 323 | return $array; 324 | } 325 | 326 | /** 327 | * Returns the recommendations that were not met. 328 | * 329 | * @return array Array of Requirement instances 330 | */ 331 | public function getFailedRecommendations() 332 | { 333 | $array = array(); 334 | foreach ($this->requirements as $req) { 335 | if (!$req->isFulfilled() && $req->isOptional()) { 336 | $array[] = $req; 337 | } 338 | } 339 | 340 | return $array; 341 | } 342 | 343 | /** 344 | * Returns whether a php.ini configuration is not correct. 345 | * 346 | * @return bool php.ini configuration problem? 347 | */ 348 | public function hasPhpIniConfigIssue() 349 | { 350 | foreach ($this->requirements as $req) { 351 | if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) { 352 | return true; 353 | } 354 | } 355 | 356 | return false; 357 | } 358 | 359 | /** 360 | * Returns the PHP configuration file (php.ini) path. 361 | * 362 | * @return string|false php.ini file path 363 | */ 364 | public function getPhpIniConfigPath() 365 | { 366 | return get_cfg_var('cfg_file_path'); 367 | } 368 | } 369 | 370 | /** 371 | * This class specifies all requirements and optional recommendations that 372 | * are necessary to run the Symfony Standard Edition. 373 | * 374 | * @author Tobias Schultze 375 | * @author Fabien Potencier 376 | */ 377 | class SymfonyRequirements extends RequirementCollection 378 | { 379 | const REQUIRED_PHP_VERSION = '5.3.3'; 380 | 381 | /** 382 | * Constructor that initializes the requirements. 383 | */ 384 | public function __construct() 385 | { 386 | /* mandatory requirements follow */ 387 | 388 | $installedPhpVersion = phpversion(); 389 | 390 | $this->addRequirement( 391 | version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>='), 392 | sprintf('PHP version must be at least %s (%s installed)', self::REQUIRED_PHP_VERSION, $installedPhpVersion), 393 | sprintf('You are running PHP version "%s", but Symfony needs at least PHP "%s" to run. 394 | Before using Symfony, upgrade your PHP installation, preferably to the latest version.', 395 | $installedPhpVersion, self::REQUIRED_PHP_VERSION), 396 | sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) 397 | ); 398 | 399 | $this->addRequirement( 400 | version_compare($installedPhpVersion, '5.3.16', '!='), 401 | 'PHP version must not be 5.3.16 as Symfony won\'t work properly with it', 402 | 'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)' 403 | ); 404 | 405 | $this->addRequirement( 406 | is_dir(__DIR__.'/../vendor/composer'), 407 | 'Vendor libraries must be installed', 408 | 'Vendor libraries are missing. Install composer following instructions from http://getcomposer.org/. '. 409 | 'Then run "php composer.phar install" to install them.' 410 | ); 411 | 412 | $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache'; 413 | 414 | $this->addRequirement( 415 | is_writable($cacheDir), 416 | 'app/cache/ or var/cache/ directory must be writable', 417 | 'Change the permissions of either "app/cache/" or "var/cache/" directory so that the web server can write into it.' 418 | ); 419 | 420 | $logsDir = is_dir(__DIR__.'/../var/logs') ? __DIR__.'/../var/logs' : __DIR__.'/logs'; 421 | 422 | $this->addRequirement( 423 | is_writable($logsDir), 424 | 'app/logs/ or var/logs/ directory must be writable', 425 | 'Change the permissions of either "app/logs/" or "var/logs/" directory so that the web server can write into it.' 426 | ); 427 | 428 | $this->addPhpIniRequirement( 429 | 'date.timezone', true, false, 430 | 'date.timezone setting must be set', 431 | 'Set the "date.timezone" setting in php.ini* (like Europe/Paris).' 432 | ); 433 | 434 | if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { 435 | $timezones = array(); 436 | foreach (DateTimeZone::listAbbreviations() as $abbreviations) { 437 | foreach ($abbreviations as $abbreviation) { 438 | $timezones[$abbreviation['timezone_id']] = true; 439 | } 440 | } 441 | 442 | $this->addRequirement( 443 | isset($timezones[@date_default_timezone_get()]), 444 | sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()), 445 | 'Your default timezone is not supported by PHP. Check for typos in your php.ini file and have a look at the list of deprecated timezones at http://php.net/manual/en/timezones.others.php.' 446 | ); 447 | } 448 | 449 | $this->addRequirement( 450 | function_exists('iconv'), 451 | 'iconv() must be available', 452 | 'Install and enable the iconv extension.' 453 | ); 454 | 455 | $this->addRequirement( 456 | function_exists('json_encode'), 457 | 'json_encode() must be available', 458 | 'Install and enable the JSON extension.' 459 | ); 460 | 461 | $this->addRequirement( 462 | function_exists('session_start'), 463 | 'session_start() must be available', 464 | 'Install and enable the session extension.' 465 | ); 466 | 467 | $this->addRequirement( 468 | function_exists('ctype_alpha'), 469 | 'ctype_alpha() must be available', 470 | 'Install and enable the ctype extension.' 471 | ); 472 | 473 | $this->addRequirement( 474 | function_exists('token_get_all'), 475 | 'token_get_all() must be available', 476 | 'Install and enable the Tokenizer extension.' 477 | ); 478 | 479 | $this->addRequirement( 480 | function_exists('simplexml_import_dom'), 481 | 'simplexml_import_dom() must be available', 482 | 'Install and enable the SimpleXML extension.' 483 | ); 484 | 485 | if (function_exists('apc_store') && ini_get('apc.enabled')) { 486 | if (version_compare($installedPhpVersion, '5.4.0', '>=')) { 487 | $this->addRequirement( 488 | version_compare(phpversion('apc'), '3.1.13', '>='), 489 | 'APC version must be at least 3.1.13 when using PHP 5.4', 490 | 'Upgrade your APC extension (3.1.13+).' 491 | ); 492 | } else { 493 | $this->addRequirement( 494 | version_compare(phpversion('apc'), '3.0.17', '>='), 495 | 'APC version must be at least 3.0.17', 496 | 'Upgrade your APC extension (3.0.17+).' 497 | ); 498 | } 499 | } 500 | 501 | $this->addPhpIniRequirement('detect_unicode', false); 502 | 503 | if (extension_loaded('suhosin')) { 504 | $this->addPhpIniRequirement( 505 | 'suhosin.executor.include.whitelist', 506 | create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'), 507 | false, 508 | 'suhosin.executor.include.whitelist must be configured correctly in php.ini', 509 | 'Add "phar" to suhosin.executor.include.whitelist in php.ini*.' 510 | ); 511 | } 512 | 513 | if (extension_loaded('xdebug')) { 514 | $this->addPhpIniRequirement( 515 | 'xdebug.show_exception_trace', false, true 516 | ); 517 | 518 | $this->addPhpIniRequirement( 519 | 'xdebug.scream', false, true 520 | ); 521 | 522 | $this->addPhpIniRecommendation( 523 | 'xdebug.max_nesting_level', 524 | create_function('$cfgValue', 'return $cfgValue > 100;'), 525 | true, 526 | 'xdebug.max_nesting_level should be above 100 in php.ini', 527 | 'Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.' 528 | ); 529 | } 530 | 531 | $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null; 532 | 533 | $this->addRequirement( 534 | null !== $pcreVersion, 535 | 'PCRE extension must be available', 536 | 'Install the PCRE extension (version 8.0+).' 537 | ); 538 | 539 | if (extension_loaded('mbstring')) { 540 | $this->addPhpIniRequirement( 541 | 'mbstring.func_overload', 542 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 543 | true, 544 | 'string functions should not be overloaded', 545 | 'Set "mbstring.func_overload" to 0 in php.ini* to disable function overloading by the mbstring extension.' 546 | ); 547 | } 548 | 549 | /* optional recommendations follow */ 550 | 551 | if (file_exists(__DIR__.'/../vendor/composer')) { 552 | require_once __DIR__.'/../vendor/autoload.php'; 553 | 554 | try { 555 | $r = new ReflectionClass('Sensio\Bundle\DistributionBundle\SensioDistributionBundle'); 556 | 557 | $contents = file_get_contents(dirname($r->getFileName()).'/Resources/skeleton/app/SymfonyRequirements.php'); 558 | } catch (ReflectionException $e) { 559 | $contents = ''; 560 | } 561 | $this->addRecommendation( 562 | file_get_contents(__FILE__) === $contents, 563 | 'Requirements file should be up-to-date', 564 | 'Your requirements file is outdated. Run composer install and re-check your configuration.' 565 | ); 566 | } 567 | 568 | $this->addRecommendation( 569 | version_compare($installedPhpVersion, '5.3.4', '>='), 570 | 'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions', 571 | 'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.' 572 | ); 573 | 574 | $this->addRecommendation( 575 | version_compare($installedPhpVersion, '5.3.8', '>='), 576 | 'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156', 577 | 'Install PHP 5.3.8 or newer if your project uses annotations.' 578 | ); 579 | 580 | $this->addRecommendation( 581 | version_compare($installedPhpVersion, '5.4.0', '!='), 582 | 'You should not use PHP 5.4.0 due to the PHP bug #61453', 583 | 'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.' 584 | ); 585 | 586 | $this->addRecommendation( 587 | version_compare($installedPhpVersion, '5.4.11', '>='), 588 | 'When using the logout handler from the Symfony Security Component, you should have at least PHP 5.4.11 due to PHP bug #63379 (as a workaround, you can also set invalidate_session to false in the security logout handler configuration)', 589 | 'Install PHP 5.4.11 or newer if your project uses the logout handler from the Symfony Security Component.' 590 | ); 591 | 592 | $this->addRecommendation( 593 | (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<')) 594 | || 595 | version_compare($installedPhpVersion, '5.4.8', '>='), 596 | 'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909', 597 | 'Install PHP 5.3.18+ or PHP 5.4.8+ if you want nice error messages for all fatal errors in the development environment.' 598 | ); 599 | 600 | if (null !== $pcreVersion) { 601 | $this->addRecommendation( 602 | $pcreVersion >= 8.0, 603 | sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion), 604 | 'PCRE 8.0+ is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Symfony probably works anyway but it is recommended to upgrade your PCRE extension.' 605 | ); 606 | } 607 | 608 | $this->addRecommendation( 609 | class_exists('DomDocument'), 610 | 'PHP-DOM and PHP-XML modules should be installed', 611 | 'Install and enable the PHP-DOM and the PHP-XML modules.' 612 | ); 613 | 614 | $this->addRecommendation( 615 | function_exists('mb_strlen'), 616 | 'mb_strlen() should be available', 617 | 'Install and enable the mbstring extension.' 618 | ); 619 | 620 | $this->addRecommendation( 621 | function_exists('iconv'), 622 | 'iconv() should be available', 623 | 'Install and enable the iconv extension.' 624 | ); 625 | 626 | $this->addRecommendation( 627 | function_exists('utf8_decode'), 628 | 'utf8_decode() should be available', 629 | 'Install and enable the XML extension.' 630 | ); 631 | 632 | $this->addRecommendation( 633 | function_exists('filter_var'), 634 | 'filter_var() should be available', 635 | 'Install and enable the filter extension.' 636 | ); 637 | 638 | if (!defined('PHP_WINDOWS_VERSION_BUILD')) { 639 | $this->addRecommendation( 640 | function_exists('posix_isatty'), 641 | 'posix_isatty() should be available', 642 | 'Install and enable the php_posix extension (used to colorize the CLI output).' 643 | ); 644 | } 645 | 646 | $this->addRecommendation( 647 | extension_loaded('intl'), 648 | 'intl extension should be available', 649 | 'Install and enable the intl extension (used for validators).' 650 | ); 651 | 652 | if (extension_loaded('intl')) { 653 | // in some WAMP server installations, new Collator() returns null 654 | $this->addRecommendation( 655 | null !== new Collator('fr_FR'), 656 | 'intl extension should be correctly configured', 657 | 'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.' 658 | ); 659 | 660 | // check for compatible ICU versions (only done when you have the intl extension) 661 | if (defined('INTL_ICU_VERSION')) { 662 | $version = INTL_ICU_VERSION; 663 | } else { 664 | $reflector = new ReflectionExtension('intl'); 665 | 666 | ob_start(); 667 | $reflector->info(); 668 | $output = strip_tags(ob_get_clean()); 669 | 670 | preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches); 671 | $version = $matches[1]; 672 | } 673 | 674 | $this->addRecommendation( 675 | version_compare($version, '4.0', '>='), 676 | 'intl ICU version should be at least 4+', 677 | 'Upgrade your intl extension with a newer ICU version (4+).' 678 | ); 679 | 680 | $this->addPhpIniRecommendation( 681 | 'intl.error_level', 682 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 683 | true, 684 | 'intl.error_level should be 0 in php.ini', 685 | 'Set "intl.error_level" to "0" in php.ini* to inhibit the messages when an error occurs in ICU functions.' 686 | ); 687 | } 688 | 689 | $accelerator = 690 | (extension_loaded('eaccelerator') && ini_get('eaccelerator.enable')) 691 | || 692 | (extension_loaded('apc') && ini_get('apc.enabled')) 693 | || 694 | (extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable')) 695 | || 696 | (extension_loaded('Zend OPcache') && ini_get('opcache.enable')) 697 | || 698 | (extension_loaded('xcache') && ini_get('xcache.cacher')) 699 | || 700 | (extension_loaded('wincache') && ini_get('wincache.ocenabled')) 701 | ; 702 | 703 | $this->addRecommendation( 704 | $accelerator, 705 | 'a PHP accelerator should be installed', 706 | 'Install and/or enable a PHP accelerator (highly recommended).' 707 | ); 708 | 709 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 710 | $this->addRecommendation( 711 | $this->getRealpathCacheSize() > 1000, 712 | 'realpath_cache_size should be above 1024 in php.ini', 713 | 'Set "realpath_cache_size" to e.g. "1024" in php.ini* to improve performance on windows.' 714 | ); 715 | } 716 | 717 | $this->addPhpIniRecommendation('short_open_tag', false); 718 | 719 | $this->addPhpIniRecommendation('magic_quotes_gpc', false, true); 720 | 721 | $this->addPhpIniRecommendation('register_globals', false, true); 722 | 723 | $this->addPhpIniRecommendation('session.auto_start', false); 724 | 725 | $this->addRecommendation( 726 | class_exists('PDO'), 727 | 'PDO should be installed', 728 | 'Install PDO (mandatory for Doctrine).' 729 | ); 730 | 731 | if (class_exists('PDO')) { 732 | $drivers = PDO::getAvailableDrivers(); 733 | $this->addRecommendation( 734 | count($drivers) > 0, 735 | sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), 736 | 'Install PDO drivers (mandatory for Doctrine).' 737 | ); 738 | } 739 | } 740 | 741 | /** 742 | * Loads realpath_cache_size from php.ini and converts it to int. 743 | * 744 | * (e.g. 16k is converted to 16384 int) 745 | * 746 | * @return int 747 | */ 748 | protected function getRealpathCacheSize() 749 | { 750 | $size = ini_get('realpath_cache_size'); 751 | $size = trim($size); 752 | $unit = strtolower(substr($size, -1, 1)); 753 | switch ($unit) { 754 | case 'g': 755 | return $size * 1024 * 1024 * 1024; 756 | case 'm': 757 | return $size * 1024 * 1024; 758 | case 'k': 759 | return $size * 1024; 760 | default: 761 | return (int) $size; 762 | } 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /blog/app/autoload.php: -------------------------------------------------------------------------------- 1 | getPhpIniConfigPath(); 8 | 9 | echo_title('Symfony2 Requirements Checker'); 10 | 11 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 12 | if ($iniPath) { 13 | echo_style('green', ' '.$iniPath); 14 | } else { 15 | echo_style('warning', ' WARNING: No configuration file (php.ini) used by PHP!'); 16 | } 17 | 18 | echo PHP_EOL.PHP_EOL; 19 | 20 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 21 | 22 | $messages = array(); 23 | foreach ($symfonyRequirements->getRequirements() as $req) { 24 | /** @var $req Requirement */ 25 | if ($helpText = get_error_message($req, $lineSize)) { 26 | echo_style('red', 'E'); 27 | $messages['error'][] = $helpText; 28 | } else { 29 | echo_style('green', '.'); 30 | } 31 | } 32 | 33 | $checkPassed = empty($messages['error']); 34 | 35 | foreach ($symfonyRequirements->getRecommendations() as $req) { 36 | if ($helpText = get_error_message($req, $lineSize)) { 37 | echo_style('yellow', 'W'); 38 | $messages['warning'][] = $helpText; 39 | } else { 40 | echo_style('green', '.'); 41 | } 42 | } 43 | 44 | if ($checkPassed) { 45 | echo_block('success', 'OK', 'Your system is ready to run Symfony2 projects'); 46 | } else { 47 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony2 projects'); 48 | 49 | echo_title('Fix the following mandatory requirements', 'red'); 50 | 51 | foreach ($messages['error'] as $helpText) { 52 | echo ' * '.$helpText.PHP_EOL; 53 | } 54 | } 55 | 56 | if (!empty($messages['warning'])) { 57 | echo_title('Optional recommendations to improve your setup', 'yellow'); 58 | 59 | foreach ($messages['warning'] as $helpText) { 60 | echo ' * '.$helpText.PHP_EOL; 61 | } 62 | } 63 | 64 | echo PHP_EOL; 65 | echo_style('title', 'Note'); 66 | echo ' The command console could use a different php.ini file'.PHP_EOL; 67 | echo_style('title', '~~~~'); 68 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 69 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 70 | echo ' server using the '; 71 | echo_style('yellow', 'web/config.php'); 72 | echo ' script.'.PHP_EOL; 73 | echo PHP_EOL; 74 | 75 | exit($checkPassed ? 0 : 1); 76 | 77 | function get_error_message(Requirement $requirement, $lineSize) 78 | { 79 | if ($requirement->isFulfilled()) { 80 | return; 81 | } 82 | 83 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 84 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 85 | 86 | return $errorMessage; 87 | } 88 | 89 | function echo_title($title, $style = null) 90 | { 91 | $style = $style ?: 'title'; 92 | 93 | echo PHP_EOL; 94 | echo_style($style, $title.PHP_EOL); 95 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 96 | echo PHP_EOL; 97 | } 98 | 99 | function echo_style($style, $message) 100 | { 101 | // ANSI color codes 102 | $styles = array( 103 | 'reset' => "\033[0m", 104 | 'red' => "\033[31m", 105 | 'green' => "\033[32m", 106 | 'yellow' => "\033[33m", 107 | 'error' => "\033[37;41m", 108 | 'success' => "\033[37;42m", 109 | 'title' => "\033[34m", 110 | ); 111 | $supports = has_color_support(); 112 | 113 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 114 | } 115 | 116 | function echo_block($style, $title, $message) 117 | { 118 | $message = ' '.trim($message).' '; 119 | $width = strlen($message); 120 | 121 | echo PHP_EOL.PHP_EOL; 122 | 123 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 124 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT).PHP_EOL); 125 | echo_style($style, str_pad($message, $width, ' ', STR_PAD_RIGHT).PHP_EOL); 126 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 127 | } 128 | 129 | function has_color_support() 130 | { 131 | static $support; 132 | 133 | if (null === $support) { 134 | if (DIRECTORY_SEPARATOR == '\\') { 135 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 136 | } else { 137 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 138 | } 139 | } 140 | 141 | return $support; 142 | } 143 | -------------------------------------------------------------------------------- /blog/app/config/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: parameters.yml } 3 | - { resource: security.yml } 4 | - { resource: services.yml } 5 | - { resource: "@AppBundle/Resources/config/services.yml" } 6 | 7 | # Put parameters here that don't need to change on each machine where the app is deployed 8 | # http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 9 | parameters: 10 | locale: en 11 | 12 | framework: 13 | #esi: ~ 14 | #translator: { fallbacks: ["%locale%"] } 15 | secret: "%secret%" 16 | router: 17 | resource: "%kernel.root_dir%/config/routing.yml" 18 | strict_requirements: ~ 19 | form: ~ 20 | csrf_protection: ~ 21 | validation: { enable_annotations: true } 22 | #serializer: { enable_annotations: true } 23 | templating: 24 | engines: ['twig'] 25 | #assets_version: SomeVersionScheme 26 | default_locale: "%locale%" 27 | trusted_hosts: ~ 28 | trusted_proxies: ~ 29 | session: 30 | # handler_id set to null will use default session handler from php.ini 31 | handler_id: ~ 32 | fragments: ~ 33 | http_method_override: true 34 | 35 | # Twig Configuration 36 | twig: 37 | debug: "%kernel.debug%" 38 | strict_variables: "%kernel.debug%" 39 | 40 | # Assetic Configuration 41 | assetic: 42 | debug: "%kernel.debug%" 43 | use_controller: false 44 | bundles: [ ] 45 | #java: /usr/bin/java 46 | filters: 47 | cssrewrite: ~ 48 | #closure: 49 | # jar: "%kernel.root_dir%/Resources/java/compiler.jar" 50 | #yui_css: 51 | # jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar" 52 | 53 | # Doctrine Configuration 54 | doctrine: 55 | dbal: 56 | driver: pdo_mysql 57 | host: "%database_host%" 58 | port: "%database_port%" 59 | dbname: "%database_name%" 60 | user: "%database_user%" 61 | password: "%database_password%" 62 | charset: UTF8 63 | # if using pdo_sqlite as your database driver: 64 | # 1. add the path in parameters.yml 65 | # e.g. database_path: "%kernel.root_dir%/data/data.db3" 66 | # 2. Uncomment database_path in parameters.yml.dist 67 | # 3. Uncomment next line: 68 | # path: "%database_path%" 69 | 70 | orm: 71 | auto_generate_proxy_classes: "%kernel.debug%" 72 | naming_strategy: doctrine.orm.naming_strategy.underscore 73 | auto_mapping: true 74 | 75 | # Swiftmailer Configuration 76 | swiftmailer: 77 | transport: "%mailer_transport%" 78 | host: "%mailer_host%" 79 | username: "%mailer_user%" 80 | password: "%mailer_password%" 81 | spool: { type: memory } 82 | 83 | doctrine_mongodb: 84 | connections: 85 | default: 86 | server: %mongo_database_server% 87 | options: {} 88 | default_database: %mongo_database_name% 89 | document_managers: 90 | default: 91 | auto_mapping: true 92 | mappings: 93 | app: 94 | type: yml 95 | dir: %kernel.root_dir%/../src/Infrastructure/ODM/Mapping/ 96 | prefix: Infrastructure\ODM\Document 97 | is_bundle: false 98 | -------------------------------------------------------------------------------- /blog/app/config/config_dev.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | 4 | framework: 5 | router: 6 | resource: "%kernel.root_dir%/config/routing_dev.yml" 7 | strict_requirements: true 8 | profiler: { only_exceptions: false } 9 | 10 | web_profiler: 11 | toolbar: true 12 | intercept_redirects: false 13 | 14 | monolog: 15 | handlers: 16 | main: 17 | type: stream 18 | path: "%kernel.logs_dir%/%kernel.environment%.log" 19 | level: debug 20 | console: 21 | type: console 22 | bubble: false 23 | verbosity_levels: 24 | VERBOSITY_VERBOSE: INFO 25 | VERBOSITY_VERY_VERBOSE: DEBUG 26 | channels: ["!doctrine"] 27 | console_very_verbose: 28 | type: console 29 | bubble: false 30 | verbosity_levels: 31 | VERBOSITY_VERBOSE: NOTICE 32 | VERBOSITY_VERY_VERBOSE: NOTICE 33 | VERBOSITY_DEBUG: DEBUG 34 | channels: ["doctrine"] 35 | # uncomment to get logging in your browser 36 | # you may have to allow bigger header sizes in your Web server configuration 37 | #firephp: 38 | # type: firephp 39 | # level: info 40 | #chromephp: 41 | # type: chromephp 42 | # level: info 43 | 44 | assetic: 45 | use_controller: true 46 | 47 | #swiftmailer: 48 | # delivery_address: me@example.com 49 | -------------------------------------------------------------------------------- /blog/app/config/config_prod.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | 4 | #framework: 5 | # validation: 6 | # cache: validator.mapping.cache.apc 7 | # serializer: 8 | # cache: serializer.mapping.cache.apc 9 | 10 | #doctrine: 11 | # orm: 12 | # metadata_cache_driver: apc 13 | # result_cache_driver: apc 14 | # query_cache_driver: apc 15 | 16 | monolog: 17 | handlers: 18 | main: 19 | type: fingers_crossed 20 | action_level: error 21 | handler: nested 22 | nested: 23 | type: stream 24 | path: "%kernel.logs_dir%/%kernel.environment%.log" 25 | level: debug 26 | console: 27 | type: console 28 | -------------------------------------------------------------------------------- /blog/app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config_dev.yml } 3 | 4 | framework: 5 | test: ~ 6 | session: 7 | storage_id: session.storage.mock_file 8 | profiler: 9 | collect: false 10 | 11 | web_profiler: 12 | toolbar: false 13 | intercept_redirects: false 14 | 15 | swiftmailer: 16 | disable_delivery: true 17 | -------------------------------------------------------------------------------- /blog/app/config/parameters.yml.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of what your parameters.yml file should look like 2 | # Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production. 3 | # http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | parameters: 5 | database_host: 127.0.0.1 6 | database_port: ~ 7 | database_name: symfony 8 | database_user: root 9 | database_password: ~ 10 | # You should uncomment this if you want use pdo_sqlite 11 | # database_path: "%kernel.root_dir%/data.db3" 12 | 13 | mailer_transport: smtp 14 | mailer_host: 127.0.0.1 15 | mailer_user: ~ 16 | mailer_password: ~ 17 | 18 | # A secret key that's used to generate certain security-related tokens 19 | secret: ThisTokenIsNotSoSecretChangeIt 20 | 21 | mongo_database_server: 'mongodb://db:27017' 22 | mongo_database_name: phpcondemo -------------------------------------------------------------------------------- /blog/app/config/routing.yml: -------------------------------------------------------------------------------- 1 | app: 2 | resource: "@AppBundle/Resources/config/routing.yml" 3 | -------------------------------------------------------------------------------- /blog/app/config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _wdt: 2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" 3 | prefix: /_wdt 4 | 5 | _profiler: 6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" 7 | prefix: /_profiler 8 | 9 | _configurator: 10 | resource: "@SensioDistributionBundle/Resources/config/routing/webconfigurator.xml" 11 | prefix: /_configurator 12 | 13 | _errors: 14 | resource: "@TwigBundle/Resources/config/routing/errors.xml" 15 | prefix: /_error 16 | 17 | _main: 18 | resource: routing.yml 19 | -------------------------------------------------------------------------------- /blog/app/config/security.yml: -------------------------------------------------------------------------------- 1 | # To get started with security, check out the documentation: 2 | # http://symfony.com/doc/current/book/security.html 3 | security: 4 | 5 | # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers 6 | providers: 7 | in_memory: 8 | memory: ~ 9 | 10 | firewalls: 11 | # disables authentication for assets and the profiler, adapt it according to your needs 12 | dev: 13 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 14 | security: false 15 | 16 | main: 17 | anonymous: ~ 18 | # activate different ways to authenticate 19 | 20 | # http_basic: ~ 21 | # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate 22 | 23 | # form_login: ~ 24 | # http://symfony.com/doc/current/cookbook/security/form_login_setup.html 25 | -------------------------------------------------------------------------------- /blog/app/config/services.yml: -------------------------------------------------------------------------------- 1 | # Learn more about services, parameters and containers at 2 | # http://symfony.com/doc/current/book/service_container.html 3 | parameters: 4 | # parameter_name: value 5 | 6 | services: 7 | # service_name: 8 | # class: AppBundle\Directory\ClassName 9 | # arguments: ["@another_service_name", "plain_value", "%parameter_name%"] 10 | -------------------------------------------------------------------------------- /blog/app/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); 19 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; 20 | 21 | if ($debug) { 22 | Debug::enable(); 23 | } 24 | 25 | $kernel = new AppKernel($env, $debug); 26 | $application = new Application($kernel); 27 | $application->run($input); 28 | -------------------------------------------------------------------------------- /blog/app/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulff/phpcon-demo/e888d81925399692ffdb6f9f3b5f8cb6cce2970f/blog/app/logs/.gitkeep -------------------------------------------------------------------------------- /blog/app/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | ../src/*/*Bundle/Tests 13 | ../src/*/Bundle/*Bundle/Tests 14 | ../src/*Bundle/Tests 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | ../src 27 | 28 | ../src/*Bundle/Resources 29 | ../src/*Bundle/Tests 30 | ../src/*/*Bundle/Resources 31 | ../src/*/*Bundle/Tests 32 | ../src/*/Bundle/*Bundle/Resources 33 | ../src/*/Bundle/*Bundle/Tests 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /blog/behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | domain: 4 | paths: [ %paths.base%/features ] 5 | contexts: 6 | - DomainContext 7 | filters: 8 | tags: @domain 9 | 10 | extensions: 11 | Behat\Symfony2Extension: 12 | kernel: 13 | env: test 14 | Behat\MinkExtension: 15 | sessions: 16 | default: 17 | symfony2: ~ 18 | Codifico\ParameterBagExtension\ServiceContainer\ParameterBagExtension: 19 | parameter_bag: 20 | class: Codifico\ParameterBagExtension\Bag\InMemoryPlaceholderBag -------------------------------------------------------------------------------- /blog/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ulff/blog", 3 | "license": "proprietary", 4 | "type": "project", 5 | "autoload": { 6 | "psr-4": { 7 | "": "src/" 8 | } 9 | }, 10 | "require": { 11 | "php": ">=5.3.9", 12 | "symfony/symfony": "2.7.*", 13 | "doctrine/orm": "^2.4.8", 14 | "doctrine/doctrine-bundle": "~1.4", 15 | "symfony/assetic-bundle": "~2.3", 16 | "symfony/swiftmailer-bundle": "~2.3", 17 | "symfony/monolog-bundle": "~2.4", 18 | "sensio/distribution-bundle": "~4.0", 19 | "sensio/framework-extra-bundle": "^3.0.2", 20 | "incenteev/composer-parameter-handler": "~2.0", 21 | "phpspec/phpspec": "^2.3", 22 | "doctrine/data-fixtures": "^1.1", 23 | "doctrine/doctrine-fixtures-bundle": "^2.3", 24 | "everzet/persisted-objects": "^1.0", 25 | "doctrine/mongodb-odm-bundle": "^3.0", 26 | "behat/symfony2-extension": "^2.1", 27 | "codifico/parameter-bag-extension": "dev-master", 28 | "behat/mink-goutte-driver": "^1.2", 29 | "behat/mink-extension": "^2.1", 30 | "behat/behat": "^3.0" 31 | }, 32 | "require-dev": { 33 | "sensio/generator-bundle": "~2.3" 34 | }, 35 | "scripts": { 36 | "post-install-cmd": [ 37 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 38 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 39 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 40 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 41 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 42 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 43 | ], 44 | "post-update-cmd": [ 45 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 46 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 47 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 48 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 49 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 50 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 51 | ] 52 | }, 53 | "config": { 54 | "bin-dir": "bin" 55 | }, 56 | "extra": { 57 | "symfony-app-dir": "app", 58 | "symfony-web-dir": "web", 59 | "symfony-assets-install": "relative", 60 | "incenteev-parameters": { 61 | "file": "app/config/parameters.yml" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /blog/features/addingComment.feature: -------------------------------------------------------------------------------- 1 | Feature: adding comment to post 2 | 3 | @domain 4 | Scenario: add comment to post 5 | Given post exists with data: 6 | | title | Adding comment | 7 | | content | This post was created as a background for adding comments test | 8 | When I add new comment for that post with data: 9 | | author | Barrack Osama | 10 | | content | This is content of added comment | 11 | Then new comment should be added to that post -------------------------------------------------------------------------------- /blog/features/bootstrap/DomainContext.php: -------------------------------------------------------------------------------- 1 | eventBus = new EventBus(); 86 | $this->eventStorage = new InMemoryEventStorage(); 87 | $this->projectionStorage = new InMemoryProjectionStorage(); 88 | $this->publishPost = new PublishPost($this->eventBus, $this->eventStorage); 89 | $this->updatePost = new UpdatePost($this->eventBus, $this->eventStorage); 90 | $this->addComment = new AddComment($this->eventBus, $this->eventStorage); 91 | $this->listPosts = new ListPosts($this->projectionStorage); 92 | 93 | new PostListListener($this->eventBus, $this->projectionStorage); 94 | } 95 | 96 | /** 97 | * @When I create post with data: 98 | * @Given post exists with data: 99 | */ 100 | public function iCreatePostWithData(TableNode $table) 101 | { 102 | $this->publishPost->execute(new PublishPost\Command( 103 | $table->getRowsHash()['title'], 104 | $table->getRowsHash()['content'] 105 | ), $this); 106 | } 107 | 108 | /** 109 | * @When I update post with data: 110 | */ 111 | public function iUpdatePostWithData(TableNode $table) 112 | { 113 | $this->updatePost->execute(new UpdatePost\Command( 114 | $this->currentPost->getPostId(), 115 | $table->getRowsHash()['title'], 116 | $table->getRowsHash()['content'] 117 | ), $this); 118 | } 119 | 120 | /** 121 | * @Then new post should be published 122 | */ 123 | public function newPostShouldBePublished() 124 | { 125 | if(empty($this->currentPost)) { 126 | throw new \Exception('Post was not published!'); 127 | } 128 | } 129 | 130 | /** 131 | * @Then post should be updated 132 | */ 133 | public function postShouldBeUpdated() 134 | { 135 | if(empty($this->currentPost)) { 136 | throw new \Exception('Post does not exist!'); 137 | } 138 | if($this->postWasUpdated === false) { 139 | throw new \Exception('Post was not updated!'); 140 | } 141 | } 142 | 143 | /** 144 | * @When I add new comment for that post with data: 145 | */ 146 | public function iAddNewCommentForThatPostWithData(TableNode $table) 147 | { 148 | $this->addComment->execute(new AddComment\Command( 149 | $this->currentPost->getPostId(), 150 | $table->getRowsHash()['author'], 151 | $table->getRowsHash()['content'] 152 | ), $this); 153 | } 154 | 155 | /** 156 | * @Then new comment should be added to that post 157 | */ 158 | public function newCommentShouldBeAddedToThatPost() 159 | { 160 | if(empty($this->currentComment)) { 161 | throw new \Exception('Comment was not added!'); 162 | } 163 | if($this->currentComment->getPostId() != $this->currentPost->getPostId()) { 164 | throw new \Exception('Comment was not added, but is not assigned to proper post!'); 165 | } 166 | } 167 | 168 | /** 169 | * @When I visit post list 170 | */ 171 | public function iVisitPostList() 172 | { 173 | $this->listPosts->execute(new ListPosts\Command(), $this); 174 | } 175 | 176 | /** 177 | * @Then I should see post :postTitle on the list 178 | */ 179 | public function iShouldSeePostOnTheList($postTitle) 180 | { 181 | foreach($this->postListProjections as $postListProjection) { 182 | if($postListProjection->title == $postTitle) { 183 | return; 184 | } 185 | } 186 | throw new \Exception('Expected post titled "'.$postTitle.'" was not found on the list'); 187 | } 188 | 189 | /** 190 | * @Then I should not see post :postTitle on the list 191 | */ 192 | public function iShouldNotSeePostOnTheList($postTitle) 193 | { 194 | foreach($this->postListProjections as $postListProjection) { 195 | if($postListProjection->title == $postTitle) { 196 | throw new \Exception('Post titled "'.$postTitle.'" exists on the list, but was not expected'); 197 | } 198 | } 199 | } 200 | 201 | /** 202 | * @param Post $post 203 | */ 204 | public function postPublishedSuccessfully(Post $post) 205 | { 206 | $this->currentPost = $post; 207 | } 208 | 209 | /** 210 | * @param Post $post 211 | */ 212 | public function postUpdatedSuccessfully(Post $post) 213 | { 214 | $this->currentPost = $post; 215 | $this->postWasUpdated = true; 216 | } 217 | 218 | /** 219 | * @param Comment $comment 220 | */ 221 | public function commentAddedSuccessfully(Comment $comment) 222 | { 223 | $this->currentComment = $comment; 224 | } 225 | 226 | /** 227 | * @param PostListProjection[] $postListProjections 228 | */ 229 | public function postsListedSuccessfully(array $postListProjections) 230 | { 231 | $this->postListProjections = $postListProjections; 232 | } 233 | 234 | /** 235 | * @param \Exception $e 236 | */ 237 | public function postPublishingFailed(\Exception $e) 238 | { 239 | throw new \Exception($e->getMessage()); 240 | } 241 | 242 | /** 243 | * @param \Exception $e 244 | */ 245 | public function commentAddingFailed(\Exception $e) 246 | { 247 | throw new \Exception($e->getMessage()); 248 | } 249 | 250 | /** 251 | * @param \Exception $e 252 | */ 253 | public function postUpdatingFailed(\Exception $e) 254 | { 255 | throw new \Exception($e->getMessage()); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /blog/features/listingPosts.feature: -------------------------------------------------------------------------------- 1 | Feature: listing posts 2 | 3 | @domain 4 | Scenario: listing posts 5 | Given post exists with data: 6 | | title | Listing posts | 7 | | content | This post was created as a test for listing posts | 8 | When I visit post list 9 | Then I should see post "Listing posts" on the list -------------------------------------------------------------------------------- /blog/features/publishingPost.feature: -------------------------------------------------------------------------------- 1 | Feature: publishing post 2 | 3 | @domain 4 | Scenario: publishing post 5 | When I create post with data: 6 | | title | Publishing post | 7 | | content | This post was created as a test for publishing posts | 8 | Then new post should be published 9 | 10 | @domain 11 | Scenario: published post should occur on post list 12 | When I create post with data: 13 | | title | Publishing another post | 14 | | content | Another post which was created as a test for publishing posts | 15 | And I visit post list 16 | Then I should see post "Publishing another post" on the list -------------------------------------------------------------------------------- /blog/features/updatingPost.feature: -------------------------------------------------------------------------------- 1 | Feature: updating post 2 | 3 | Background: 4 | Given post exists with data: 5 | | title | Updating posts | 6 | | content | This post was created as a test for updating posts | 7 | 8 | @domain 9 | Scenario: updating post 10 | When I update post with data: 11 | | title | Post after update | 12 | | content | This post was updated once | 13 | Then post should be updated 14 | 15 | @domain 16 | Scenario: updated post should affect post list 17 | When I update post with data: 18 | | title | Post after update | 19 | | content | This post was updated once | 20 | And I visit post list 21 | Then I should see post "Post after update" on the list 22 | Then I should not see post "Updating posts" on the list -------------------------------------------------------------------------------- /blog/spec/Domain/Aggregate/AggregateId/CommentIdSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(Argument::type('Domain\Aggregate\AggregateId\CommentId')); 13 | } 14 | 15 | function it_is_initializable() 16 | { 17 | $this->shouldHaveType('Domain\Aggregate\AggregateId\CommentId'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blog/spec/Domain/Aggregate/AggregateId/PostIdSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(Argument::type('Domain\Aggregate\AggregateId\PostId')); 13 | } 14 | 15 | function it_is_initializable() 16 | { 17 | $this->shouldHaveType('Domain\Aggregate\AggregateId\PostId'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blog/spec/Domain/Aggregate/CommentSpec.php: -------------------------------------------------------------------------------- 1 | postId = PostId::generate(); 36 | $this->author = 'Ellis'; 37 | $this->content = 'Bob has written this'; 38 | 39 | $this->beConstructedThrough('create', [$this->postId, $this->author, $this->content]); 40 | } 41 | 42 | function it_is_initializable() 43 | { 44 | $this->shouldHaveType('Domain\Aggregate\Comment'); 45 | } 46 | 47 | function it_is_an_aggregate() 48 | { 49 | $this->shouldImplement(Aggregate::class); 50 | } 51 | 52 | function it_has_reference_to_post() 53 | { 54 | $this->getPostId()->shouldReturn($this->postId); 55 | } 56 | 57 | function it_has_content() 58 | { 59 | $this->getContent()->shouldReturn($this->content); 60 | } 61 | 62 | function it_has_author() 63 | { 64 | $this->getAuthor()->shouldReturn($this->author); 65 | } 66 | 67 | function it_has_creating_date() 68 | { 69 | $this->getCreatingDate()->shouldHaveType('DateTime'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /blog/spec/Domain/Aggregate/PostSpec.php: -------------------------------------------------------------------------------- 1 | title = 'Short leading post'; 29 | $this->content = 'This is first post created on this blog'; 30 | 31 | $this->beConstructedThrough('create', [$this->title, $this->content]); 32 | } 33 | 34 | function it_is_initializable() 35 | { 36 | $this->shouldHaveType('Domain\Aggregate\Post'); 37 | } 38 | 39 | function it_is_an_aggregate() 40 | { 41 | $this->shouldImplement(Aggregate::class); 42 | } 43 | 44 | function it_has_title() 45 | { 46 | $this->getTitle()->shouldReturn($this->title); 47 | } 48 | 49 | function it_has_content() 50 | { 51 | $this->getContent()->shouldReturn($this->content); 52 | } 53 | 54 | function it_has_publishing_date() 55 | { 56 | $this->getPublishingDate()->shouldHaveType('DateTime'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /blog/spec/Domain/Event/CommentWasAddedSpec.php: -------------------------------------------------------------------------------- 1 | commentId = CommentId::generate(); 44 | $this->postId = PostId::generate(); 45 | $this->author = 'Rochelle'; 46 | $this->content = 'Comment'; 47 | $this->creatingDate = new \DateTime(); 48 | 49 | $this->beConstructedWith($this->commentId, $this->postId, $this->author, $this->content, $this->creatingDate); 50 | } 51 | 52 | function it_is_initializable() 53 | { 54 | $this->shouldHaveType('Domain\Event\CommentWasAdded'); 55 | } 56 | 57 | function it_has_aggregate_id() 58 | { 59 | $this->getAggregateId()->shouldHaveType('Domain\Aggregate\AggregateId\CommentId'); 60 | $this->getAggregateId()->shouldReturn($this->commentId); 61 | } 62 | 63 | function it_has_post_id() 64 | { 65 | $this->getPostId()->shouldHaveType('Domain\Aggregate\AggregateId\PostId'); 66 | $this->getPostId()->shouldReturn($this->postId); 67 | } 68 | 69 | function it_has_author() 70 | { 71 | $this->getAuthor()->shouldReturn($this->author); 72 | } 73 | 74 | function it_has_content() 75 | { 76 | $this->getContent()->shouldReturn($this->content); 77 | } 78 | 79 | function it_has_creating_date() 80 | { 81 | $this->getCreatingDate()->shouldHaveType(\DateTime::class); 82 | $this->getCreatingDate()->shouldReturn($this->creatingDate); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /blog/spec/Domain/Event/PostWasPublishedSpec.php: -------------------------------------------------------------------------------- 1 | postId = PostId::generate(); 38 | $this->title = 'title'; 39 | $this->content = 'content'; 40 | $this->publishingDate = new \DateTime(); 41 | 42 | $this->beConstructedWith($this->postId, $this->title, $this->content, $this->publishingDate); 43 | } 44 | 45 | function it_is_initializable() 46 | { 47 | $this->shouldHaveType('Domain\Event\PostWasPublished'); 48 | } 49 | 50 | function it_has_aggregate_id() 51 | { 52 | $this->getAggregateId()->shouldHaveType('Domain\Aggregate\AggregateId\PostId'); 53 | $this->getAggregateId()->shouldReturn($this->postId); 54 | } 55 | 56 | function it_has_title() 57 | { 58 | $this->getTitle()->shouldReturn($this->title); 59 | } 60 | 61 | function it_has_content() 62 | { 63 | $this->getContent()->shouldReturn($this->content); 64 | } 65 | 66 | function it_has_publishing_date() 67 | { 68 | $this->getPublishingDate()->shouldHaveType(\DateTime::class); 69 | $this->getPublishingDate()->shouldReturn($this->publishingDate); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /blog/spec/Domain/Event/PostWasUpdatedSpec.php: -------------------------------------------------------------------------------- 1 | postId = PostId::generate(); 33 | $this->title = 'title'; 34 | $this->content = 'content'; 35 | 36 | $this->beConstructedWith($this->postId, $this->title, $this->content); 37 | } 38 | 39 | function it_is_initializable() 40 | { 41 | $this->shouldHaveType('Domain\Event\PostWasUpdated'); 42 | } 43 | 44 | function it_has_aggregate_id() 45 | { 46 | $this->getAggregateId()->shouldHaveType('Domain\Aggregate\AggregateId\PostId'); 47 | $this->getAggregateId()->shouldReturn($this->postId); 48 | } 49 | 50 | function it_has_title() 51 | { 52 | $this->getTitle()->shouldReturn($this->title); 53 | } 54 | 55 | function it_has_content() 56 | { 57 | $this->getContent()->shouldReturn($this->content); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /blog/spec/Domain/EventEngine/EventBusSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Domain\EventEngine\EventBus'); 20 | } 21 | 22 | function it_should_trigger_post_list_projection_update(PostListListener $postListListener) 23 | { 24 | $postListListener->when(Argument::type(PostWasPublished::class))->shouldBeCalled(); 25 | 26 | $this->registerListener($postListListener); 27 | $this->dispatch([new PostWasPublished(PostId::generate(), 'title', 'content', new \DateTime())]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /blog/spec/Domain/ReadModel/Listener/PostListListenerSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($eventBus, $projectionStorage); 22 | } 23 | 24 | function it_is_initializable() 25 | { 26 | $this->shouldHaveType('Domain\ReadModel\Listener\PostListListener'); 27 | } 28 | 29 | function it_should_update_post_list_projection(ProjectionStorage $projectionStorage) 30 | { 31 | $event = new PostWasPublished(PostId::generate(), 'title', 'content', new \DateTime()); 32 | $projectionStorage->save(Argument::type(PostListProjection::class))->shouldBeCalled(); 33 | 34 | $this->when($event); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blog/spec/Domain/ReadModel/Populator/PostListPopulatorSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($eventStorage, $projectionStorage, $listener); 19 | } 20 | 21 | function it_is_initializable() 22 | { 23 | $this->shouldHaveType('Domain\ReadModel\Populator\PostListPopulator'); 24 | } 25 | 26 | function it_should_have_projection_name() 27 | { 28 | $this->getProjectionName()->shouldReturn('post-list'); 29 | } 30 | 31 | function it_should_have_stats() 32 | { 33 | $this->getStats()->shouldHaveType('Domain\ReadModel\ProjectionPopulatorStats'); 34 | } 35 | 36 | function it_should_clear_projections(BulkProjectionStorage $projectionStorage) 37 | { 38 | $projectionStorage->find('post-list')->willReturn( 39 | $projections = [new PostListProjection(PostId::generate(), 'Post title', new \DateTime())] 40 | ); 41 | 42 | foreach ($projections as $projection) { 43 | $projectionStorage->remove($projection)->shouldBeCalled(); 44 | } 45 | 46 | $projectionStorage->flush()->shouldBeCalled(); 47 | 48 | $this->clear(); 49 | } 50 | 51 | function it_should_run_populator( 52 | BulkProjectionStorage $projectionStorage, 53 | EventStorage $eventStorage, 54 | DomainEventListener $listener 55 | ) { 56 | $projectionStorage->find('post-list')->willReturn( 57 | $projections = [new PostListProjection(PostId::generate(), 'Post title', new \DateTime())] 58 | ); 59 | 60 | foreach ($projections as $projection) { 61 | $projectionStorage->remove($projection)->shouldBeCalled(); 62 | } 63 | 64 | $projectionStorage->flush()->shouldBeCalled(); 65 | 66 | $eventStorage->getAll()->willReturn( 67 | $events = [new PostWasPublished(PostId::generate(), 'title', 'content', new \DateTime())] 68 | ); 69 | 70 | foreach ($events as $event) { 71 | $eventClass = explode('\\', get_class($event)); 72 | $method = 'on'.end($eventClass); 73 | if (method_exists($listener, $method)) { 74 | $listener->when($event)->shouldBeCalled(); 75 | } 76 | } 77 | $projectionStorage->flush()->shouldBeCalled(); 78 | 79 | $this->run(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /blog/spec/Domain/ReadModel/Projection/PostListProjectionSpec.php: -------------------------------------------------------------------------------- 1 | postId = PostId::generate(); 34 | $this->title = 'Post title'; 35 | $this->publishingDate = new \DateTime(); 36 | 37 | $this->beConstructedWith($this->postId, $this->title, $this->publishingDate); 38 | } 39 | 40 | function it_is_initializable() 41 | { 42 | $this->shouldHaveType('Domain\ReadModel\Projection\PostListProjection'); 43 | } 44 | 45 | function it_is_a_projection() 46 | { 47 | $this->shouldImplement(Projection::class); 48 | } 49 | 50 | function it_has_projection_name() 51 | { 52 | $this->getProjectionName()->shouldReturn('post-list'); 53 | } 54 | 55 | function it_has_aggregate_id() 56 | { 57 | $this->getAggregateId()->shouldReturn($this->postId); 58 | } 59 | 60 | function it_has_title() 61 | { 62 | $this->getTitle()->shouldReturn($this->title); 63 | } 64 | 65 | function it_has_publishing_date() 66 | { 67 | $this->getPublishingDate()->shouldReturn($this->publishingDate); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /blog/src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /blog/src/AppBundle/AppBundle.php: -------------------------------------------------------------------------------- 1 | %command.name% command removes all items from selected projection. 19 | \tphp %command.full_name% --name=populator-name 20 | TXT; 21 | 22 | $this 23 | ->setName('ulff:projection:clear') 24 | ->setDescription('Clears selected projection') 25 | ->addOption( 26 | 'name', 27 | null, 28 | InputOption::VALUE_REQUIRED, 29 | 'Name of the projection you want to clear', 30 | null 31 | ) 32 | ->setHelp($help); 33 | } 34 | 35 | /** 36 | * @param InputInterface $input 37 | * @param OutputInterface $output 38 | * @return int|null|void 39 | */ 40 | protected function execute(InputInterface $input, OutputInterface $output) 41 | { 42 | $this->validateParameters($input, $output); 43 | 44 | $populator = $this->getPopulator(); 45 | $populator->clear(); 46 | 47 | $output->writeln('Finished!'); 48 | $output->writeln( 49 | sprintf( 50 | 'Removed %s projections', 51 | $populator->getStats()->getRemoved() 52 | ) 53 | ); 54 | 55 | return; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /blog/src/AppBundle/Command/LoadFixturesCommand.php: -------------------------------------------------------------------------------- 1 | %command.name% command loads defined fixtures. Usage: '."\n"; 17 | $help .= 'php %command.full_name% [--force=true]'; 18 | 19 | $this 20 | ->setName('ulff:fixtures:load') 21 | ->setDescription('Loads defined fixtures') 22 | ->setHelp($help) 23 | ->addOption( 24 | 'force', 25 | null, 26 | InputOption::VALUE_OPTIONAL, 27 | 'Forces fixtures to be loaded when EventStorage is not empty.', 28 | false 29 | ); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $fixturesExecutor = new FixturesExecutor( 35 | $this->getContainer()->get('event_bus'), 36 | $this->getContainer()->get('event_storage'), 37 | $this->getContainer()->get('projection_storage') 38 | ); 39 | 40 | if($input->getOption('force') == 'true') { 41 | $fixturesExecutor->force(); 42 | } 43 | 44 | try { 45 | $fixturesExecutor->run(); 46 | } catch (EventStorageNotEmptyException $e) { 47 | $output->writeln(sprintf('EventStorage is not empty. Please clear EventStorage or use command with --force=true option.')); 48 | return; 49 | } 50 | 51 | $output->writeln( 52 | sprintf( 53 | 'Fixtures have been loaded.' 54 | ) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /blog/src/AppBundle/Command/PopulateProjectionCommand.php: -------------------------------------------------------------------------------- 1 | %command.name% command populates (or re-populates) selected projection. 19 | \tphp %command.full_name% --name=populator-name 20 | TXT; 21 | 22 | $this 23 | ->setName('ulff:projection:populate') 24 | ->setDescription('Populates (or re-populates) selected projection') 25 | ->addOption( 26 | 'name', 27 | null, 28 | InputOption::VALUE_REQUIRED, 29 | 'Name of the projection you want to (re)populate', 30 | null 31 | ) 32 | ->setHelp($help); 33 | } 34 | 35 | /** 36 | * @param InputInterface $input 37 | * @param OutputInterface $output 38 | * @return int|null|void 39 | */ 40 | protected function execute(InputInterface $input, OutputInterface $output) 41 | { 42 | $this->validateParameters($input, $output); 43 | 44 | $populator = $this->getPopulator(); 45 | $populator->run(); 46 | 47 | $output->writeln('Finished!'); 48 | $output->writeln( 49 | sprintf( 50 | 'Removed %d projections', 51 | $populator->getStats()->getRemoved() 52 | ) 53 | ); 54 | $output->writeln( 55 | sprintf( 56 | 'Processed %d of total %d events from EventStorage', 57 | $populator->getStats()->getProcessedEvents(), 58 | $populator->getStats()->getTotalEvents() 59 | ) 60 | ); 61 | $output->writeln( 62 | sprintf( 63 | 'Loaded %d new projections', 64 | $populator->getStats()->getPopulatedProjections() 65 | ) 66 | ); 67 | } 68 | } -------------------------------------------------------------------------------- /blog/src/AppBundle/Command/ProjectionCommand.php: -------------------------------------------------------------------------------- 1 | getOption('name'); 34 | if (empty($name)) { 35 | $errorMessage = sprintf('Empty required parameter name'); 36 | 37 | throw new \Exception($errorMessage, ProjectionCommand::EXIT_CODE_EMPTY_PARAMETER); 38 | } 39 | 40 | $this->resolveClassName($name); 41 | if (!class_exists($this->populatorClassName)) { 42 | $errorMessage = sprintf( 43 | 'Invalid class prefix: %s, class %s does not exist', 44 | $name, 45 | $this->populatorClassName 46 | ); 47 | 48 | throw new \Exception($errorMessage, ProjectionCommand::EXIT_CODE_INVALID_PREFIX); 49 | } 50 | } 51 | 52 | /** 53 | * @param string $name 54 | */ 55 | protected function resolveClassName($name) 56 | { 57 | $prefix = str_replace('-', '', ucwords($name, '-')); 58 | $this->populatorClassName = 'Domain\\ReadModel\\Populator\\'.$prefix.'Populator'; 59 | $this->listenerClassName = 'Domain\\ReadModel\\Listener\\'.$prefix.'Listener'; 60 | } 61 | 62 | /** 63 | * @return ProjectionPopulator 64 | */ 65 | protected function getPopulator() 66 | { 67 | /** @var $populator ProjectionPopulator */ 68 | $populator = new $this->populatorClassName( 69 | $this->getContainer()->get('event_storage'), 70 | $this->getContainer()->get('bulk_projection_storage'), 71 | new $this->listenerClassName( 72 | $this->getContainer()->get('event_bus'), 73 | $this->getContainer()->get('bulk_projection_storage') 74 | ) 75 | ); 76 | 77 | return $populator; 78 | } 79 | } -------------------------------------------------------------------------------- /blog/src/AppBundle/Controller/BlogController.php: -------------------------------------------------------------------------------- 1 | get('projection_storage')); 20 | $listPostsUseCase->execute(new ListPosts\Command(), $this); 21 | 22 | return $this->render('AppBundle:Blog:list.html.twig', ['posts' => $this->postList]); 23 | } 24 | 25 | public function viewAction() 26 | { 27 | throw new HttpException(404, 'Not implemented yet'); 28 | } 29 | 30 | public function createAction() 31 | { 32 | throw new HttpException(404, 'Not implemented yet'); 33 | } 34 | 35 | /** {@inheritdoc} */ 36 | public function postsListedSuccessfully(array $projections) 37 | { 38 | $this->postList = []; 39 | /** @var PostListProjection $projection */ 40 | foreach ($projections as $projection) { 41 | $this->postList[] = PostListItem::createFromProjection($projection); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /blog/src/AppBundle/Entity/FromAggregate.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 16 | $this->title = $title; 17 | $this->publishingDate = $publishingDate; 18 | } 19 | 20 | /** 21 | * @param Projection $projection 22 | * @return FromProjection 23 | */ 24 | public static function createFromProjection(Projection $projection) 25 | { 26 | /** @var $projection Projection\PostListProjection */ 27 | return new self( 28 | (string) $projection->getAggregateId(), 29 | $projection->getTitle(), 30 | $projection->getPublishingDate()->format('Y-m-d H:i:s') 31 | ); 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getId() 38 | { 39 | return $this->postId; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getTitle() 46 | { 47 | return $this->title; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getPublishingDate() 54 | { 55 | return $this->publishingDate; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /blog/src/AppBundle/Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | post_list: 2 | path: /posts 3 | defaults: { _controller: AppBundle:Blog:list } 4 | 5 | post_view: 6 | path: /posts/{id} 7 | defaults: { _controller: AppBundle:Blog:view } 8 | requirements: 9 | id: \w+ 10 | 11 | post_create: 12 | path: /posts/create 13 | defaults: { _controller: AppBundle:Blog:create } 14 | 15 | root: 16 | path: / 17 | defaults: 18 | _controller: FrameworkBundle:Redirect:redirect 19 | route: post_list 20 | permanent: true -------------------------------------------------------------------------------- /blog/src/AppBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | event_bus: 3 | class: Domain\EventEngine\EventBus 4 | calls: 5 | - [registerListener, ["@listener.post_list"]] 6 | 7 | event_storage: 8 | class: Infrastructure\ODM\ODMEventStorage 9 | arguments: ['@doctrine.odm.mongodb.document_manager', '@doctrine_mongodb'] 10 | 11 | projection_storage: 12 | class: Infrastructure\ODM\ODMProjectionStorage 13 | arguments: ['@doctrine.odm.mongodb.document_manager', '@doctrine_mongodb'] 14 | 15 | bulk_projection_storage: 16 | class: Infrastructure\ODM\ODMBulkProjectionStorage 17 | arguments: ['@doctrine.odm.mongodb.document_manager', '@doctrine_mongodb'] 18 | 19 | listener.post_list: 20 | class: Domain\ReadModel\Listener\PostListListener 21 | arguments: ['@event_bus', '@projection_storage'] -------------------------------------------------------------------------------- /blog/src/AppBundle/Resources/views/Blog/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}PHPCon demo blog!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | 10 | {% block body %}{% endblock %} 11 | {% block javascripts %}{% endblock %} 12 | {% block content %}{% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /blog/src/AppBundle/Resources/views/Blog/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "AppBundle:Blog:base.html.twig" %} 2 | 3 | {% block content %} 4 | {% for flashMessage in app.session.flashbag.get('notice') %} 5 |
{{ flashMessage }}
6 | {% endfor %} 7 | 8 |

PHPcon demo blog

9 | 10 | {% if posts is not empty %} 11 | 12 | 13 | 16 | 19 | 22 | 23 | {% for post in posts %} 24 | 25 | 28 | 31 | 33 | 34 | {% endfor %} 35 |
14 | Name 15 | 17 | Publishing date 18 | 20 | Actions 21 |
26 | {{ post.title }} 27 | 29 | {{ post.getPublishingDate }} 30 | 32 |
36 | {% else %} 37 | No posts 38 | {% endif %} 39 | 40 |

41 | Create new post 42 |

43 | {% endblock %} -------------------------------------------------------------------------------- /blog/src/Domain/Aggregate/AggregateId/CommentId.php: -------------------------------------------------------------------------------- 1 | commentId = $commentId; 14 | } 15 | 16 | public static function fromString($string) 17 | { 18 | $commentId = new self($string); 19 | 20 | return $commentId; 21 | } 22 | 23 | public function __toString() 24 | { 25 | return $this->commentId; 26 | } 27 | 28 | public static function generate() 29 | { 30 | $random = md5(mt_rand(time()-10, time()) . 'comment'); 31 | 32 | return self::fromString($random); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blog/src/Domain/Aggregate/AggregateId/PostId.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 14 | } 15 | 16 | public static function fromString($string) 17 | { 18 | $postId = new self($string); 19 | 20 | return $postId; 21 | } 22 | 23 | public function __toString() 24 | { 25 | return $this->postId; 26 | } 27 | 28 | public static function generate() 29 | { 30 | $random = md5(mt_rand(time()-10, time()) . 'post'); 31 | 32 | return self::fromString($random); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blog/src/Domain/Aggregate/Comment.php: -------------------------------------------------------------------------------- 1 | commentId = $commentId; 49 | } 50 | 51 | /** 52 | * @return PostId 53 | */ 54 | public function getPostId() 55 | { 56 | return $this->postId; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getContent() 63 | { 64 | return $this->content; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function getAuthor() 71 | { 72 | return $this->author; 73 | } 74 | 75 | /** 76 | * @return \DateTime 77 | */ 78 | public function getCreatingDate() 79 | { 80 | return $this->creatingDate; 81 | } 82 | 83 | /** 84 | * @param string $content 85 | */ 86 | private function setContent($content) 87 | { 88 | $this->content = $content; 89 | } 90 | 91 | /** 92 | * @param string $author 93 | */ 94 | private function setAuthor($author) 95 | { 96 | $this->author = $author; 97 | } 98 | 99 | /** 100 | * @param \DateTime $creatingDate 101 | */ 102 | private function setCreatingDate($creatingDate) 103 | { 104 | $this->creatingDate = $creatingDate; 105 | } 106 | 107 | /** 108 | * @param PostId $postId 109 | */ 110 | private function setPostId($postId) 111 | { 112 | $this->postId = $postId; 113 | } 114 | 115 | /** 116 | * @return CommentId 117 | */ 118 | public function getAggregateId() 119 | { 120 | return $this->commentId; 121 | } 122 | 123 | private function applyCommentWasAdded(CommentWasAdded $event) 124 | { 125 | $this->setPostId($event->getPostId()); 126 | $this->setAuthor($event->getAuthor()); 127 | $this->setContent($event->getContent()); 128 | $this->setCreatingDate($event->getCreatingDate()); 129 | } 130 | 131 | /** 132 | * @param PostId $postId 133 | * @param string $author 134 | * @param string $content 135 | * @return Comment 136 | */ 137 | public static function create(PostId $postId, $author, $content) 138 | { 139 | $comment = new self($commentId = CommentId::generate()); 140 | $comment->setPostId($postId); 141 | $comment->setAuthor($author); 142 | $comment->setContent($content); 143 | $comment->setCreatingDate($creatingDate = new \DateTime()); 144 | $comment->recordThat(new CommentWasAdded($commentId, $postId, $author, $content, $creatingDate)); 145 | 146 | return $comment; 147 | } 148 | 149 | /** 150 | * @param CommentAggregateHistory $commentAggregateHistory 151 | * @return Post 152 | */ 153 | public static function reconstituteFrom(AggregateHistory $commentAggregateHistory) 154 | { 155 | $comment = new self($commentAggregateHistory->getAggregateId()); 156 | 157 | foreach ($commentAggregateHistory->getEvents() as $event) { 158 | $applyMethod = explode('\\', get_class($event)); 159 | $applyMethod = 'apply' . end($applyMethod); 160 | $comment->$applyMethod($event); 161 | } 162 | 163 | return $comment; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /blog/src/Domain/Aggregate/Post.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 40 | } 41 | 42 | public function getPostId() 43 | { 44 | return $this->postId; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getTitle() 51 | { 52 | return $this->title; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getContent() 59 | { 60 | return $this->content; 61 | } 62 | 63 | /** 64 | * @return \DateTime 65 | */ 66 | public function getPublishingDate() 67 | { 68 | return $this->publishingDate; 69 | } 70 | 71 | /** 72 | * @param string $title 73 | */ 74 | private function setTitle($title) 75 | { 76 | $this->title = $title; 77 | } 78 | 79 | /** 80 | * @param string $content 81 | */ 82 | private function setContent($content) 83 | { 84 | $this->content = $content; 85 | } 86 | 87 | /** 88 | * @param \DateTime $publishingDate 89 | */ 90 | private function setPublishingDate($publishingDate) 91 | { 92 | $this->publishingDate = $publishingDate; 93 | } 94 | 95 | /** 96 | * @return PostId 97 | */ 98 | public function getAggregateId() 99 | { 100 | return $this->postId; 101 | } 102 | 103 | /** 104 | * @param string $title 105 | * @param string $content 106 | */ 107 | public function update($title, $content) 108 | { 109 | $this->recordThat($event = new PostWasUpdated($this->getAggregateId(), $title, $content)); 110 | $this->apply($event); 111 | } 112 | 113 | private function applyPostWasPublished(PostWasPublished $event) 114 | { 115 | $this->setTitle($event->getTitle()); 116 | $this->setContent($event->getContent()); 117 | $this->setPublishingDate($event->getPublishingDate()); 118 | } 119 | 120 | private function applyPostWasUpdated(PostWasUpdated $event) 121 | { 122 | $this->setTitle($event->getTitle()); 123 | $this->setContent($event->getContent()); 124 | } 125 | 126 | /** 127 | * @param string $title 128 | * @param string $content 129 | * @return Post 130 | */ 131 | public static function create($title, $content) 132 | { 133 | $post = new self($postId = PostId::generate()); 134 | $post->setTitle($title); 135 | $post->setContent($content); 136 | $post->setPublishingDate($publishingDate = new \DateTime()); 137 | $post->recordThat(new PostWasPublished($postId, $title, $content, $publishingDate)); 138 | 139 | return $post; 140 | } 141 | 142 | /** 143 | * @param PostAggregateHistory $postAggregateHistory 144 | * @return Post 145 | */ 146 | public static function reconstituteFrom(AggregateHistory $postAggregateHistory) 147 | { 148 | $post = new self($postAggregateHistory->getAggregateId()); 149 | 150 | foreach ($postAggregateHistory->getEvents() as $event) { 151 | $applyMethod = explode('\\', get_class($event)); 152 | $applyMethod = 'apply' . end($applyMethod); 153 | $post->$applyMethod($event); 154 | } 155 | 156 | return $post; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /blog/src/Domain/AggregateHistory/CommentAggregateHistory.php: -------------------------------------------------------------------------------- 1 | commentId = $commentId; 46 | $this->postId = $postId; 47 | $this->author = $author; 48 | $this->content = $content; 49 | $this->creatingDate = $creatingDate; 50 | } 51 | 52 | /** 53 | * @return CommentId 54 | */ 55 | public function getAggregateId() 56 | { 57 | return $this->commentId; 58 | } 59 | 60 | /** 61 | * @return PostId 62 | */ 63 | public function getPostId() 64 | { 65 | return $this->postId; 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getAuthor() 72 | { 73 | return $this->author; 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | public function getContent() 80 | { 81 | return $this->content; 82 | } 83 | 84 | /** 85 | * @return \DateTime 86 | */ 87 | public function getCreatingDate() 88 | { 89 | return $this->creatingDate; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /blog/src/Domain/Event/PostWasPublished.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 39 | $this->title = $title; 40 | $this->content = $content; 41 | $this->publishingDate = $publishingDate; 42 | } 43 | 44 | /** 45 | * @return PostId 46 | */ 47 | public function getAggregateId() 48 | { 49 | return $this->postId; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getTitle() 56 | { 57 | return $this->title; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getContent() 64 | { 65 | return $this->content; 66 | } 67 | 68 | /** 69 | * @return \DateTime 70 | */ 71 | public function getPublishingDate() 72 | { 73 | return $this->publishingDate; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /blog/src/Domain/Event/PostWasUpdated.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 33 | $this->title = $title; 34 | $this->content = $content; 35 | } 36 | 37 | /** 38 | * @return PostId 39 | */ 40 | public function getAggregateId() 41 | { 42 | return $this->postId; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getTitle() 49 | { 50 | return $this->title; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getContent() 57 | { 58 | return $this->content; 59 | } 60 | } -------------------------------------------------------------------------------- /blog/src/Domain/EventEngine/Aggregate.php: -------------------------------------------------------------------------------- 1 | aggregateId = $aggregateId; 24 | $this->events = $this->events = $eventStorage->find($aggregateId); 25 | } 26 | 27 | /** 28 | * @return AggregateId 29 | */ 30 | public function getAggregateId() 31 | { 32 | return $this->aggregateId; 33 | } 34 | 35 | /** 36 | * @return DomainEvent[] 37 | */ 38 | public function getEvents() 39 | { 40 | return $this->events; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blog/src/Domain/EventEngine/DomainEvent.php: -------------------------------------------------------------------------------- 1 | listeners[] = $domainEventListener; 17 | } 18 | 19 | /** 20 | * @param $events DomainEvent[] 21 | */ 22 | public function dispatch(array $events) 23 | { 24 | /** @var DomainEvent $event */ 25 | foreach ($events as $event) { 26 | /** @var DomainEventListener $listener */ 27 | foreach ($this->listeners as $listener) { 28 | $listener->when($event); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /blog/src/Domain/EventEngine/EventSourced.php: -------------------------------------------------------------------------------- 1 | events; 18 | } 19 | 20 | /** 21 | * @param DomainEvent $event 22 | */ 23 | protected function recordThat(DomainEvent $event) 24 | { 25 | $this->events[] = $event; 26 | } 27 | 28 | /** 29 | * @param DomainEvent $event 30 | */ 31 | private function apply(DomainEvent $event) 32 | { 33 | $method = explode('\\', get_class($event)); 34 | $method = 'apply' . end($method); 35 | $this->$method($event); 36 | } 37 | 38 | /** 39 | * @return AggregateId 40 | */ 41 | abstract public function getAggregateId(); 42 | 43 | public static function create() 44 | { 45 | throw new \Exception('Method create was not implemented in class: '.self::class); 46 | } 47 | 48 | public static function reconstituteFrom() 49 | { 50 | throw new \Exception('Method reconstituteFrom was not implemented in class: '.self::class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /blog/src/Domain/EventEngine/EventStorage.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 29 | $this->eventStorage = $eventStorage; 30 | $this->projectionStorage = $projectionStorage; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /blog/src/Domain/FixturesEngine/Data/PostFixtures.php: -------------------------------------------------------------------------------- 1 | loadPost1($referenceRepository); 22 | $this->loadPost2($referenceRepository); 23 | $this->loadPost3($referenceRepository); 24 | } 25 | 26 | /** {@inheritdoc} */ 27 | public function getOrder() 28 | { 29 | return 1; 30 | } 31 | 32 | /** {@inheritdoc} */ 33 | public function postPublishedSuccessfully(Post $post) 34 | { 35 | $this->publishedPost = $post; 36 | } 37 | 38 | /** {@inheritdoc} */ 39 | public function postPublishingFailed(\Exception $e) 40 | { 41 | throw new \Exception($e); 42 | } 43 | 44 | private function loadPost1(ReferenceRepository $referenceRepository) 45 | { 46 | $command = new PublishPost\Command( 47 | 'First post title', 48 | 'Some content here' 49 | ); 50 | 51 | $publishPostUseCase = new PublishPost( 52 | $this->eventBus, 53 | $this->eventStorage 54 | ); 55 | $publishPostUseCase->execute($command, $this); 56 | 57 | $referenceRepository->addReference('post_1', $this->publishedPost); 58 | } 59 | 60 | private function loadPost2(ReferenceRepository $referenceRepository) 61 | { 62 | $command = new PublishPost\Command( 63 | 'PHPcon demo', 64 | 'Here is content of PHPcon demo blogpost' 65 | ); 66 | 67 | $publishPostUseCase = new PublishPost( 68 | $this->eventBus, 69 | $this->eventStorage 70 | ); 71 | $publishPostUseCase->execute($command, $this); 72 | 73 | $referenceRepository->addReference('post_2', $this->publishedPost); 74 | } 75 | 76 | private function loadPost3(ReferenceRepository $referenceRepository) 77 | { 78 | $command = new PublishPost\Command( 79 | 'Yet another post title', 80 | 'I have no ideas of content that can be put here' 81 | ); 82 | 83 | $publishPostUseCase = new PublishPost( 84 | $this->eventBus, 85 | $this->eventStorage 86 | ); 87 | $publishPostUseCase->execute($command, $this); 88 | 89 | $referenceRepository->addReference('post_3', $this->publishedPost); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /blog/src/Domain/FixturesEngine/EventStorageNotEmptyException.php: -------------------------------------------------------------------------------- 1 | message = "EventStorage is not empty!"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blog/src/Domain/FixturesEngine/FixtureInterface.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 45 | $this->eventStorage = $eventStorage; 46 | $this->projectionStorage = $projectionStorage; 47 | $this->referenceRepository = new ReferenceRepository(); 48 | $this->orderedFixtures = []; 49 | $this->forced = false; 50 | } 51 | 52 | public function run() 53 | { 54 | $this->checkEventStorage(); 55 | $this->fetchFixtures(); 56 | $this->sortFixtures(); 57 | $this->execFixtures(); 58 | } 59 | 60 | public function force() 61 | { 62 | $this->forced = true; 63 | } 64 | 65 | private function checkEventStorage() 66 | { 67 | if($this->forced === true) { 68 | return; 69 | } 70 | 71 | $storedEvents = $this->eventStorage->getAll(); 72 | foreach($storedEvents as $storedEvent) { 73 | if (get_class($storedEvent) != 'Domain\EventModel\Event\ThemeWasCreated') { 74 | throw new EventStorageNotEmptyException(); 75 | } 76 | } 77 | } 78 | 79 | private function fetchFixtures() 80 | { 81 | foreach (new \DirectoryIterator(__DIR__ . '/Data/') as $fileInfo) { 82 | if($fileInfo->getExtension() == 'php') { 83 | $className = 'Domain\\FixturesEngine\\Data\\' . basename($fileInfo->getFilename(), '.php'); 84 | if(class_exists($className) && in_array('Domain\FixturesEngine\FixtureInterface', class_implements($className))) { 85 | $this->orderedFixtures[] = new $className($this->eventBus, $this->eventStorage, $this->projectionStorage); 86 | } 87 | } 88 | } 89 | } 90 | 91 | private function sortFixtures() 92 | { 93 | uasort($this->orderedFixtures, function($a, $b) { 94 | return ($a->getOrder() < $b->getOrder()) ? -1 : 1; 95 | }); 96 | } 97 | 98 | private function execFixtures() 99 | { 100 | foreach($this->orderedFixtures as $fixture) { 101 | $fixture->load($this->referenceRepository); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /blog/src/Domain/FixturesEngine/ReferenceRepository.php: -------------------------------------------------------------------------------- 1 | repo = []; 15 | } 16 | 17 | /** 18 | * @param string $identifier 19 | * @param mixed $value 20 | */ 21 | public function addReference($identifier, $value) 22 | { 23 | $this->repo[$identifier] = $value; 24 | } 25 | 26 | /** 27 | * @param string $identifier 28 | * @return mixed 29 | */ 30 | public function getReference($identifier) 31 | { 32 | return $this->repo[$identifier]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/AbstractDomainEventListener.php: -------------------------------------------------------------------------------- 1 | registerListener($this); 22 | $this->projectionStorage = $projectionStorage; 23 | } 24 | 25 | /** 26 | * @param DomainEvent $event 27 | */ 28 | public function when(DomainEvent $event) 29 | { 30 | $method = explode('\\', get_class($event)); 31 | $method = 'on' . end($method); 32 | if (method_exists($this, $method)) { 33 | $this->$method($event); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/BulkProjectionStorage.php: -------------------------------------------------------------------------------- 1 | getAggregateId(), 17 | $event->getTitle(), 18 | $event->getPublishingDate() 19 | ); 20 | $this->projectionStorage->save($postListProjection); 21 | } 22 | 23 | public function onPostWasUpdated(PostWasUpdated $event) 24 | { 25 | $postListProjection = $this->projectionStorage->findById( 26 | 'post-list', 27 | $event->getAggregateId() 28 | ); 29 | $postListProjection->title = $event->getTitle(); 30 | $postListProjection->content = $event->getContent(); 31 | $this->projectionStorage->save($postListProjection); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/Populator/PostListPopulator.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 33 | $this->title = $title; 34 | $this->publishingDate = $publishingDate; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getProjectionName() 41 | { 42 | return self::PROJECTION_NAME; 43 | } 44 | 45 | /** 46 | * @return PostId 47 | */ 48 | public function getAggregateId() 49 | { 50 | return $this->postId; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getTitle() 57 | { 58 | return $this->title; 59 | } 60 | 61 | /** 62 | * @return \DateTime 63 | */ 64 | public function getPublishingDate() 65 | { 66 | return $this->publishingDate; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/ProjectionPopulator.php: -------------------------------------------------------------------------------- 1 | eventStorage = $eventStorage; 40 | $this->projectionStorage = $projectionStorage; 41 | $this->listener = $listener; 42 | 43 | $this->stats = new ProjectionPopulatorStats(); 44 | } 45 | 46 | public function getStats() 47 | { 48 | return $this->stats; 49 | } 50 | 51 | public function run() 52 | { 53 | $this->clear(); 54 | $this->populate(); 55 | } 56 | 57 | public function clear() 58 | { 59 | $projections = $this->projectionStorage->find($this->getProjectionName()); 60 | foreach ($projections as $projection) { 61 | $this->projectionStorage->remove($projection); 62 | $this->stats->increaseRemoved(); 63 | } 64 | $this->projectionStorage->flush(); 65 | } 66 | 67 | abstract protected function getProjectionName(); 68 | 69 | private function populate() 70 | { 71 | $events = $this->eventStorage->getAll(); 72 | foreach ($events as $event) { 73 | $this->stats->increaseTotalEvents(); 74 | $eventClass = explode('\\', get_class($event)); 75 | $method = 'on'.end($eventClass); 76 | if (method_exists($this->listener, $method)) { 77 | $this->listener->when($event); 78 | $this->stats->increaseProcessedEvents(); 79 | } 80 | } 81 | $this->projectionStorage->flush(); 82 | $this->stats->setPopulatedProjections(count($this->projectionStorage->find(static::getProjectionName()))); 83 | } 84 | } -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/ProjectionPopulatorStats.php: -------------------------------------------------------------------------------- 1 | removedProjectionsCounter = 0; 30 | $this->populatedProjectionsCounter = 0; 31 | $this->processedEventsCounter = 0; 32 | $this->totalEventsCounter = 0; 33 | } 34 | 35 | public function increaseRemoved() 36 | { 37 | ++$this->removedProjectionsCounter; 38 | } 39 | 40 | public function increaseProcessedEvents() 41 | { 42 | ++$this->processedEventsCounter; 43 | } 44 | 45 | public function increaseTotalEvents() 46 | { 47 | ++$this->totalEventsCounter; 48 | } 49 | 50 | /** 51 | * @param int $number 52 | */ 53 | public function setPopulatedProjections($number) 54 | { 55 | $this->populatedProjectionsCounter = $number; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getRemoved() 62 | { 63 | return $this->removedProjectionsCounter; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getPopulatedProjections() 70 | { 71 | return $this->populatedProjectionsCounter; 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getProcessedEvents() 78 | { 79 | return $this->processedEventsCounter; 80 | } 81 | 82 | /** 83 | * @return int 84 | */ 85 | public function getTotalEvents() 86 | { 87 | return $this->totalEventsCounter; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /blog/src/Domain/ReadModel/ProjectionStorage.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 30 | $this->eventStorage = $eventStorage; 31 | } 32 | 33 | public function execute(Command $command, Responder $responder) 34 | { 35 | $comment = Comment::create( 36 | $command->getPostId(), 37 | $command->getAuthor(), 38 | $command->getContent() 39 | ); 40 | try { 41 | $this->eventBus->dispatch($comment->getEvents()); 42 | $this->eventStorage->add($comment); 43 | } catch(\Exception $e) { 44 | $responder->commentAddingFailed($e); 45 | } 46 | 47 | $responder->commentAddedSuccessfully($comment); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/AddComment/Command.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 32 | $this->author = $author; 33 | $this->content = $content; 34 | } 35 | 36 | /** 37 | * @return PostId 38 | */ 39 | public function getPostId() 40 | { 41 | return $this->postId; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getAuthor() 48 | { 49 | return $this->author; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getContent() 56 | { 57 | return $this->content; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/AddComment/Responder.php: -------------------------------------------------------------------------------- 1 | projectionStorage = $projectionStorage; 24 | } 25 | 26 | public function execute(Command $command, Responder $responder) 27 | { 28 | $projections = $this->projectionStorage->find('post-list'); 29 | $responder->postsListedSuccessfully($projections); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/ListPosts/Command.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 30 | $this->eventStorage = $eventStorage; 31 | } 32 | 33 | public function execute(Command $command, Responder $responder) 34 | { 35 | $post = Post::create( 36 | $command->getTitle(), 37 | $command->getContent() 38 | ); 39 | try { 40 | $this->eventBus->dispatch($post->getEvents()); 41 | $this->eventStorage->add($post); 42 | } catch(\Exception $e) { 43 | $responder->postPublishingFailed($e); 44 | } 45 | 46 | $responder->postPublishedSuccessfully($post); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/PublishPost/Command.php: -------------------------------------------------------------------------------- 1 | title = $title; 24 | $this->content = $content; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getTitle() 31 | { 32 | return $this->title; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getContent() 39 | { 40 | return $this->content; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/PublishPost/Responder.php: -------------------------------------------------------------------------------- 1 | eventBus = $eventBus; 31 | $this->eventStorage = $eventStorage; 32 | } 33 | 34 | public function execute(Command $command, Responder $responder) 35 | { 36 | $postAggregateHistory = new PostAggregateHistory($command->getPostId(), $this->eventStorage); 37 | $post = Post::reconstituteFrom($postAggregateHistory); 38 | 39 | $post->update( 40 | $command->getTitle(), 41 | $command->getContent() 42 | ); 43 | 44 | try { 45 | $this->eventBus->dispatch($post->getEvents()); 46 | $this->eventStorage->add($post); 47 | } catch(\Exception $e) { 48 | $responder->postUpdatingFailed($e); 49 | } 50 | 51 | $responder->postUpdatedSuccessfully($post); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/UpdatePost/Command.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 32 | $this->title = $title; 33 | $this->content = $content; 34 | } 35 | 36 | /** 37 | * @return PostId 38 | */ 39 | public function getPostId() 40 | { 41 | return $this->postId; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getTitle() 48 | { 49 | return $this->title; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getContent() 56 | { 57 | return $this->content; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /blog/src/Domain/UseCase/UpdatePost/Responder.php: -------------------------------------------------------------------------------- 1 | aggregateId = $aggregateId; 16 | $this->aggregateClass = $aggregateClass; 17 | $this->event = serialize($event); 18 | $this->createdAt = new \DateTime(); 19 | } 20 | 21 | public function getEvent() 22 | { 23 | return unserialize($this->event); 24 | } 25 | 26 | public function getAggregateId() 27 | { 28 | return $this->aggregateId; 29 | } 30 | } -------------------------------------------------------------------------------- /blog/src/Infrastructure/InMemory/InMemoryEventStorage.php: -------------------------------------------------------------------------------- 1 | repo = new InMemoryRepository(new AccessorObjectIdentifier('getAggregateId')); 20 | } 21 | 22 | /** 23 | * @param Aggregate $aggregate 24 | */ 25 | public function add(Aggregate $aggregate) 26 | { 27 | foreach($aggregate->getEvents() as $event) { 28 | $storedEvent = new StoredEvent($aggregate->getAggregateId(), get_class($event), $event); 29 | $this->repo->save($storedEvent); 30 | } 31 | } 32 | 33 | /** 34 | * @param AggregateId $aggregateId 35 | * @return DomainEvent[] 36 | */ 37 | public function find(AggregateId $aggregateId) 38 | { 39 | $events = []; 40 | /** @var $storedEvent StoredEvent */ 41 | foreach($this->repo->getAll() as $storedEvent) { 42 | if($storedEvent->getAggregateId() == $aggregateId) { 43 | $events[] = $storedEvent->getEvent(); 44 | } 45 | } 46 | 47 | return $events; 48 | } 49 | 50 | /** 51 | * @return DomainEvent[] 52 | */ 53 | public function getAll() 54 | { 55 | $events = []; 56 | /** @var $storedEvent StoredEvent */ 57 | foreach($this->repo->getAll() as $storedEvent) { 58 | $events[] = $storedEvent->getEvent(); 59 | } 60 | 61 | return $events; 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /blog/src/Infrastructure/InMemory/InMemoryProjectionStorage.php: -------------------------------------------------------------------------------- 1 | repo = new InMemoryRepository(new AccessorObjectIdentifier('getAggregateId')); 18 | } 19 | 20 | /** 21 | * @param Projection $projection 22 | */ 23 | public function save(Projection $projection) 24 | { 25 | $this->repo->save($projection); 26 | } 27 | 28 | /** 29 | * @param Projection $projection 30 | */ 31 | public function remove(Projection $projection) 32 | { 33 | $projection = $this->repo->findById($projection->getAggregateId()); 34 | $this->repo->remove($projection); 35 | } 36 | 37 | /** 38 | * @param $projectionName 39 | * @return Projection[] 40 | */ 41 | public function find($projectionName) 42 | { 43 | $projections = []; 44 | /** @var $projection Projection */ 45 | foreach($this->repo->getAll() as $projection) { 46 | if($projection->getProjectionName() == $projectionName) { 47 | $projections[] = $projection; 48 | } 49 | } 50 | 51 | return $projections; 52 | } 53 | 54 | /** 55 | * @param string $projectionName 56 | * @param AggregateId $aggregateId 57 | * @return Projection 58 | */ 59 | public function findById($projectionName, AggregateId $aggregateId) 60 | { 61 | /** @var $projection Projection */ 62 | foreach($this->repo->getAll() as $projection) { 63 | if($projection->getProjectionName() == $projectionName && $projection->getAggregateId() == $aggregateId) { 64 | return $projection; 65 | } 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/Document/StoredEvent.php: -------------------------------------------------------------------------------- 1 | aggregateId = $aggregateId; 16 | $this->aggregateClass = $aggregateClass; 17 | $this->event = serialize($event); 18 | $this->createdAt = new \DateTime(); 19 | } 20 | 21 | public function getEvent() 22 | { 23 | return unserialize($this->event); 24 | } 25 | 26 | public function getAggregateId() 27 | { 28 | return $this->aggregateId; 29 | } 30 | } -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/Document/StoredProjection.php: -------------------------------------------------------------------------------- 1 | id = $projectionName . '_' . $aggregateId; 18 | $this->projectionName = $projectionName; 19 | $this->aggregateId = $aggregateId; 20 | $this->projection = serialize($projection); 21 | $this->updatedAt = new \DateTime(); 22 | } 23 | 24 | public function getProjection() 25 | { 26 | return unserialize($this->projection); 27 | } 28 | 29 | public function getProjectionName() 30 | { 31 | return $this->projectionName; 32 | } 33 | 34 | public function getAggregateId() 35 | { 36 | return $this->aggregateId; 37 | } 38 | } -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/Mapping/StoredEvent.mongodb.yml: -------------------------------------------------------------------------------- 1 | Infrastructure\ODM\Document\StoredEvent: 2 | fields: 3 | id: 4 | id: true 5 | strategy: AUTO 6 | aggregateId: 7 | type: string 8 | aggregateClass: 9 | type: string 10 | event: 11 | type: string 12 | createdAt: 13 | type: date 14 | -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/Mapping/StoredProjection.mongodb.yml: -------------------------------------------------------------------------------- 1 | Infrastructure\ODM\Document\StoredProjection: 2 | changeTrackingPolicy: DEFERRED_EXPLICIT 3 | fields: 4 | id: 5 | id: true 6 | type: string 7 | strategy: NONE 8 | aggregateId: 9 | type: string 10 | projectionName: 11 | type: string 12 | projection: 13 | type: string 14 | updatedAt: 15 | type: date -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/ODMBulkProjectionStorage.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 33 | $this->repository = $managerRegistry->getRepository('\Infrastructure\ODM\Document\StoredProjection'); 34 | } 35 | 36 | /** 37 | * @param Projection $projection 38 | */ 39 | public function save(Projection $projection) 40 | { 41 | $storedProjection = new StoredProjection( 42 | $projection->getProjectionName(), 43 | $projection->getAggregateId(), 44 | $projection 45 | ); 46 | $this->manager->persist($storedProjection); 47 | } 48 | 49 | /** 50 | * @param Projection $projection 51 | */ 52 | public function remove(Projection $projection) 53 | { 54 | $storedProjection = $this->repository->find($projection->getProjectionName().'_'.$projection->getAggregateId()); 55 | if (!empty($storedProjection)) { 56 | $this->manager->remove($storedProjection); 57 | } 58 | } 59 | 60 | /** 61 | * @param $projectionName 62 | * @return Projection[] 63 | */ 64 | public function find($projectionName) 65 | { 66 | $projections = []; 67 | /** @var $storedProjection StoredProjection */ 68 | $storedProjections = $this->repository->findBy(['projectionName' => (string)$projectionName]); 69 | foreach ($storedProjections as $storedProjection) { 70 | $projections[] = $storedProjection->getProjection(); 71 | } 72 | 73 | return $projections; 74 | } 75 | 76 | /** 77 | * @param $projectionName 78 | * @param AggregateId $aggregateId 79 | * @return Projection 80 | */ 81 | public function findById($projectionName, AggregateId $aggregateId) 82 | { 83 | $storedProjection = $this->repository->findOneBy( 84 | [ 85 | 'projectionName' => (string)$projectionName, 86 | 'aggregateId' => (string)$aggregateId, 87 | ] 88 | ); 89 | 90 | return empty($storedProjection) ? null : $storedProjection->getProjection(); 91 | } 92 | 93 | public function flush() 94 | { 95 | $this->manager->flush(); 96 | } 97 | } -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/ODMEventStorage.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 29 | $this->repository = $managerRegistry->getRepository('\Infrastructure\ODM\Document\StoredEvent'); 30 | } 31 | 32 | /** 33 | * @param Aggregate $aggregate 34 | */ 35 | public function add(Aggregate $aggregate) 36 | { 37 | foreach($aggregate->getEvents() as $event) { 38 | $storedEvent = new StoredEvent($aggregate->getAggregateId(), get_class($event), $event); 39 | $this->manager->persist($storedEvent); 40 | } 41 | $this->manager->flush(); 42 | } 43 | 44 | /** 45 | * @param AggregateId $aggregateId 46 | * @return DomainEvent[] 47 | */ 48 | public function find(AggregateId $aggregateId) 49 | { 50 | $events = []; 51 | /** @var $storedEvent StoredEvent */ 52 | $storedEvents = $this->repository->findBy(['aggregateId' => (string) $aggregateId]); 53 | foreach ($storedEvents as $storedEvent) { 54 | $events[] = $storedEvent->getEvent(); 55 | } 56 | 57 | return $events; 58 | } 59 | 60 | /** 61 | * @return DomainEvent[] 62 | */ 63 | public function getAll() 64 | { 65 | $events = []; 66 | /** @var $storedEvent StoredEvent */ 67 | $storedEvents = $this->repository->findAll(); 68 | foreach ($storedEvents as $storedEvent) { 69 | $events[] = $storedEvent->getEvent(); 70 | } 71 | 72 | return $events; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /blog/src/Infrastructure/ODM/ODMProjectionStorage.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 28 | $this->repository = $managerRegistry->getRepository('\Infrastructure\ODM\Document\StoredProjection'); 29 | } 30 | 31 | /** 32 | * @param Projection $projection 33 | */ 34 | public function save(Projection $projection) 35 | { 36 | $storedProjection = new StoredProjection($projection->getProjectionName(), $projection->getAggregateId(), $projection); 37 | $this->manager->persist($storedProjection); 38 | $this->manager->flush(); 39 | } 40 | 41 | /** 42 | * @param Projection $projection 43 | */ 44 | public function remove(Projection $projection) 45 | { 46 | $storedProjection = $this->repository->find($projection->getProjectionName() . '_' . $projection->getAggregateId()); 47 | if(!empty($storedProjection)) { 48 | $this->manager->remove($storedProjection); 49 | $this->manager->flush(); 50 | } 51 | } 52 | 53 | /** 54 | * @param $projectionName 55 | * @return Projection[] 56 | */ 57 | public function find($projectionName) 58 | { 59 | $projections = []; 60 | /** @var $storedProjection StoredProjection */ 61 | $storedProjections = $this->repository->findBy(['projectionName' => (string) $projectionName]); 62 | foreach ($storedProjections as $storedProjection) { 63 | $projections[] = $storedProjection->getProjection(); 64 | } 65 | 66 | return $projections; 67 | } 68 | 69 | /** 70 | * @param $projectionName 71 | * @param AggregateId $aggregateId 72 | * @return Projection 73 | */ 74 | public function findById($projectionName, AggregateId $aggregateId) 75 | { 76 | $storedProjection = $this->repository->findOneBy([ 77 | 'projectionName' => (string) $projectionName, 78 | 'aggregateId' => (string) $aggregateId 79 | ]); 80 | 81 | return empty($storedProjection) ? null : $storedProjection->getProjection(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /blog/web/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex app.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve 15 | # to the front controller "/app.php" but be rewritten to "/app.php/app". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the app.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/app.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule .? - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule .? %{ENV:BASE}/app.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 302 ^/$ /app.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /blog/web/app.php: -------------------------------------------------------------------------------- 1 | unregister(); 15 | $apcLoader->register(true); 16 | */ 17 | 18 | require_once __DIR__.'/../app/AppKernel.php'; 19 | //require_once __DIR__.'/../app/AppCache.php'; 20 | 21 | $kernel = new AppKernel('prod', false); 22 | $kernel->loadClassCache(); 23 | //$kernel = new AppCache($kernel); 24 | 25 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter 26 | //Request::enableHttpMethodParameterOverride(); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /blog/web/app_dev.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 28 | $request = Request::createFromGlobals(); 29 | $response = $kernel->handle($request); 30 | $response->send(); 31 | $kernel->terminate($request, $response); 32 | -------------------------------------------------------------------------------- /blog/web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulff/phpcon-demo/e888d81925399692ffdb6f9f3b5f8cb6cce2970f/blog/web/apple-touch-icon.png -------------------------------------------------------------------------------- /blog/web/config.php: -------------------------------------------------------------------------------- 1 | getFailedRequirements(); 20 | $minorProblems = $symfonyRequirements->getFailedRecommendations(); 21 | 22 | ?> 23 | 24 | 25 | 26 | 27 | 28 | Symfony Configuration 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 39 | 40 | 60 |
61 | 62 |
63 |
64 |
65 |

Welcome!

66 |

Welcome to your new Symfony project.

67 |

68 | This script will guide you through the basic configuration of your project. 69 | You can also do the same by editing the ‘app/config/parameters.yml’ file directly. 70 |

71 | 72 | 73 |

Major problems

74 |

Major problems have been detected and must be fixed before continuing:

75 |
    76 | 77 |
  1. getHelpHtml() ?>
  2. 78 | 79 |
80 | 81 | 82 | 83 |

Recommendations

84 |

85 | Additionally, toTo enhance your Symfony experience, 86 | it’s recommended that you fix the following: 87 |

88 |
    89 | 90 |
  1. getHelpHtml() ?>
  2. 91 | 92 |
93 | 94 | 95 | hasPhpIniConfigIssue()): ?> 96 |

* 97 | getPhpIniConfigPath()): ?> 98 | Changes to the php.ini file must be done in "getPhpIniConfigPath() ?>". 99 | 100 | To change settings, create a "php.ini". 101 | 102 |

103 | 104 | 105 | 106 |

Your configuration looks good to run Symfony.

107 | 108 | 109 | 118 |
119 |
120 |
121 |
Symfony Standard Edition
122 |
123 | 124 | 125 | -------------------------------------------------------------------------------- /blog/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulff/phpcon-demo/e888d81925399692ffdb6f9f3b5f8cb6cce2970f/blog/web/favicon.ico -------------------------------------------------------------------------------- /blog/web/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | php: 2 | build: . 3 | links: 4 | - db 5 | volumes: 6 | - .:/var/www 7 | nginx: 8 | image: nginx:1.9.4 9 | links: 10 | - php 11 | volumes_from: 12 | - php 13 | volumes: 14 | - ./docker/vhost.conf:/etc/nginx/conf.d/default.conf 15 | ports: 16 | - "80:80" 17 | db: 18 | image: mongo:latest 19 | volumes: 20 | - /mnt/sda1/var/lib/mongo-data:/data/db 21 | ports: 22 | - "27017:27017" -------------------------------------------------------------------------------- /docker/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone = Europe/Warsaw 2 | short_open_tag = off 3 | 4 | xdebug.remote_enable = on 5 | xdebug.remote_connect_back = on 6 | xdebug.remote_autostart = on 7 | xdebug.idekey = "docker" 8 | xdebug.remote_port = 9000 -------------------------------------------------------------------------------- /docker/vhost.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | client_max_body_size 10M; 5 | client_body_buffer_size 128k; 6 | server_name phpcon-demo.dev; 7 | 8 | root /var/www/blog/web; 9 | 10 | location / { 11 | # try to serve file directly, fallback to app.php 12 | index app.php; 13 | try_files $uri /app.php$is_args$args; 14 | } 15 | # DEV 16 | # This rule should only be placed on your development environment 17 | # In production, don't include this and don't deploy app_dev.php or config.php 18 | location ~ ^/(app_dev|config)\.php(/|$) { 19 | fastcgi_pass php:9000; 20 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 21 | include fastcgi_params; 22 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 23 | fastcgi_read_timeout 120; 24 | } 25 | 26 | # PROD 27 | location ~ ^/app\.php(/|$) { 28 | fastcgi_pass php:9000; 29 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 30 | include fastcgi_params; 31 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 32 | fastcgi_read_timeout 120; 33 | # Prevents URIs that include the front controller. This will 404: 34 | # http://domain.tld/app.php/some-path 35 | # Remove the internal directive to allow URIs like this 36 | internal; 37 | } 38 | } --------------------------------------------------------------------------------