├── .gitignore ├── .php-version ├── README.md ├── composer.json ├── composer.lock ├── config ├── doctrine │ └── Article.orm.xml └── router.php ├── public ├── index.php └── script.js ├── src ├── Controller │ ├── ArticleListAction.php │ ├── ArticleListOldAction.php │ ├── ArticleListOldIterableAction.php │ ├── ArticleListSymfonyAction.php │ └── StreamedJsonResponse.php ├── Doctrine │ └── EntityManagerFactory.php └── Entity │ └── Article.php └── var ├── memory-usage-old-iterable.txt ├── memory-usage-old.txt ├── memory-usage-symfony.txt ├── memory-usage.txt └── test.sqlite /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | var/cache/ 4 | -------------------------------------------------------------------------------- /.php-version: -------------------------------------------------------------------------------- 1 | 8.1 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLDR; 2 | 3 | This journey did begin with the blog post below. Today I'm happy that the blog 4 | post ended up with a contribution to Symfony Framework from my side with the great 5 | feedback we evolved the solution a lot better then I could think about it. 6 | 7 | So I'm happy that the `StreamedJsonResponse` is now part of the `symfony/http-foundation` package since 6.3: 8 | 9 | - [https://symfony.com/doc/6.4/components/http_foundation.html#streaming-a-json-response](https://symfony.com/doc/6.4/components/http_foundation.html#streaming-a-json-response) 10 | 11 | Thank to [pelmered](https://github.com/pelmered) it will also will be part of the Laravel Framework: 12 | 13 | - [https://github.com/laravel/framework/pull/49873](https://github.com/laravel/framework/pull/49873) 14 | 15 | ------ 16 | 17 | # Efficient JSON Streaming with Symfony and Doctrine 18 | 19 | After reading a tweet about we provide only a few items (max. 100) over our 20 | JSON APIs but providing 4k images for our websites. I did think about why is 21 | this the case. 22 | 23 | The main difference first we need to know about how images are streamed. 24 | On webservers today is mostly the sendfile feature used. Which is very 25 | efficient as it can stream a file chunk by chunk and don't need to load 26 | the whole data. 27 | 28 | So I'm asking myself how we can achieve the same mechanisms for our 29 | JSON APIs, with a little experiment. 30 | 31 | As an example we will have a look at a basic entity which has the 32 | following fields defined: 33 | 34 | - id: int 35 | - title: string 36 | - description: text 37 | 38 | The response of our API should look like the following: 39 | 40 | ```json 41 | { 42 | "_embedded": { 43 | "articles": [ 44 | { 45 | "id": 1, 46 | "title": "Article 1", 47 | "description": "Description 1\nMore description text ...", 48 | }, 49 | ... 50 | ] 51 | } 52 | } 53 | ``` 54 | 55 | Normally to provide this API we would do something like this: 56 | 57 | ```php 58 | findArticles($entityManager); 72 | 73 | return JsonResponse::fromJsonString(json_encode([ 74 | 'embedded' => [ 75 | 'articles' => $articles, 76 | ], 77 | 'total' => 100_000, 78 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 79 | } 80 | 81 | // normally this method would live in a repository 82 | private function findArticles(EntityManagerInterface $entityManager): iterable 83 | { 84 | $queryBuilder = $entityManager->createQueryBuilder(); 85 | $queryBuilder->from(Article::class, 'article'); 86 | $queryBuilder->select('article.id') 87 | ->addSelect('article.title') 88 | ->addSelect('article.description'); 89 | 90 | return $queryBuilder->getQuery()->getResult(); 91 | } 92 | } 93 | ``` 94 | 95 | In most cases we will add some pagination to the endpoint so our response are not too big. 96 | 97 | ## Making the api more efficient 98 | 99 | But there is also a way how we can stream this response in an efficient way. 100 | 101 | First of all we need to adjust how we load the articles. This can be done by replace 102 | the `getResult` with the more efficient [`toIterable`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/reference/batch-processing.html#iterating-results): 103 | 104 | ```diff 105 | - return $queryBuilder->getQuery()->getResult(); 106 | + return $queryBuilder->getQuery()->toIterable(); 107 | ``` 108 | 109 | Still the whole JSON need to be in the memory to send it. So we need also refactoring 110 | how we are creating our response. We will replace our `JsonResponse` with the 111 | [`StreamedResponse`](https://symfony.com/doc/6.0/components/http_foundation.html#streaming-a-response) object. 112 | 113 | ```php 114 | return new StreamedResponse(function() use ($articles) { 115 | // stream json 116 | }, 200, ['Content-Type' => 'application/json']); 117 | ``` 118 | 119 | But the `json` format is not the best format for streaming, so we need to add some hacks 120 | so we can make it streamable. 121 | 122 | First we will create will define the basic structure of our JSON this way: 123 | 124 | ```php 125 | $jsonStructure = json_encode([ 126 | 'embedded' => [ 127 | 'articles' => ['__REPLACES_ARTICLES__'], 128 | ], 129 | 'total' => 100_000, 130 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 131 | ``` 132 | 133 | Instead of the `$articles` we are using a placeholder which we use to split the string into 134 | a `$before` and `$after` variable: 135 | 136 | ```php 137 | [$before, $after] = explode('"__REPLACES_ARTICLES__"', $jsonStructure, 2); 138 | ``` 139 | 140 | Now we are first sending the `$before`: 141 | 142 | ```php 143 | echo $before . PHP_EOL; 144 | ``` 145 | 146 | Then we stream the articles one by one to it here we need to keep the comma in mind which 147 | we need to add after every article but not the last one: 148 | 149 | ```php 150 | foreach ($articles as $count => $article) { 151 | if ($count !== 0) { 152 | echo ',' . PHP_EOL; // if not first element we need a separator 153 | } 154 | 155 | echo json_encode($article, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 156 | } 157 | ``` 158 | 159 | Also we will add an additional `flush` after every 500 elements: 160 | 161 | ```php 162 | if ($count % 500 === 0 && $count !== 100_000) { // flush response after every 500 163 | flush(); 164 | } 165 | ``` 166 | 167 | After that we will also send the `$after` part: 168 | 169 | ```php 170 | echo PHP_EOL; 171 | echo $after; 172 | ``` 173 | 174 | ## The result 175 | 176 | So at the end the whole action looks like the following: 177 | 178 | ```php 179 | findArticles($entityManager); 193 | 194 | return new StreamedResponse(function() use ($articles) { 195 | // defining our json structure but replaces the articles with a placeholder 196 | $jsonStructure = json_encode([ 197 | 'embedded' => [ 198 | 'articles' => ['__REPLACES_ARTICLES__'], 199 | ], 200 | 'total' => 100_000, 201 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 202 | 203 | // split by placeholder 204 | [$before, $after] = explode('"__REPLACES_ARTICLES__"', $jsonStructure, 2); 205 | 206 | // send first before part of the json 207 | echo $before . PHP_EOL; 208 | 209 | // stream article one by one as own json 210 | foreach ($articles as $count => $article) { 211 | if ($count !== 0) { 212 | echo ',' . PHP_EOL; // if not first element we need a separator 213 | } 214 | 215 | if ($count % 500 === 0 && $count !== 100_000) { // flush response after every 500 216 | flush(); 217 | } 218 | 219 | echo json_encode($article, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 220 | } 221 | 222 | // send the after part of the json as last 223 | echo PHP_EOL; 224 | echo $after; 225 | }, 200, ['Content-Type' => 'application/json']); 226 | } 227 | 228 | private function findArticles(EntityManagerInterface $entityManager): iterable 229 | { 230 | $queryBuilder = $entityManager->createQueryBuilder(); 231 | $queryBuilder->from(Article::class, 'article'); 232 | $queryBuilder->select('article.id') 233 | ->addSelect('article.title') 234 | ->addSelect('article.description'); 235 | 236 | return $queryBuilder->getQuery()->toIterable(); 237 | } 238 | } 239 | ``` 240 | 241 | The metrics for 100000 Articles (nginx + php-fpm 7.4 - Macbook Pro 2013): 242 | 243 | | | Old Implementation | New Implementation | 244 | |---------------------------|--------------------|--------------------| 245 | | Memory Usage | 49.53 MB | 2.10 MB | 246 | | Memory Usage Peak | 59.21 MB | 2.10 MB | 247 | | Time to first Byte | 478ms | 28ms | 248 | | Time | 2.335 s | 0.584 s | 249 | 250 | This way we did not only reduce the memory usage on our server 251 | also we did make the response faster. The memory usage was 252 | measured here with `memory_get_usage` and `memory_get_peak_usage`. 253 | The "Time to first Byte" by the browser value and response times 254 | over curl. 255 | 256 | **Updated 2022-10-02 - (symfony serve + php-fpm 8.1 - Macbook Pro 2021)** 257 | 258 | | | Old Implementation | New Implementation | 259 | |---------------------------|--------------------|--------------------| 260 | | Memory Usage | 64.21 MB | 2.10 MB | 261 | | Memory Usage Peak | 73.89 MB | 2.10 MB | 262 | | Time to first Byte | 0.203 s | 0.049 s | 263 | | Updated Time (2022-10-02) | 0.233 s | 0.232 s | 264 | 265 | While there is not much different for a single response in the time, 266 | the real performance is the lower memory usage. Which will kick in when 267 | you have a lot of simultaneously requests. On my machine >150 simultaneously 268 | requests - which is a high value but will on a normal server be a lot lower. 269 | 270 | While 150 simultaneously requests crashes in the old implementation 271 | the new implementation still works with 220 simultaneously requests. Which 272 | means we got about ~46% more requests possible. 273 | 274 | ## Reading Data in javascript 275 | 276 | As we stream the data we should also make our JavaScript on the other 277 | end the same way - so data need to read in streamed way. 278 | 279 | Here I'm just following the example from the [Fetch API Processing a text file line by line](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#processing_a_text_file_line_by_line) 280 | 281 | So if we look at our [`script.js`](public/script.js) we split the object 282 | line by line and append it to our table. This method is definitely not the 283 | way how JSON should be read and parsed. It should only be shown as example 284 | how the response could be read from a stream. 285 | 286 | ## Conclusion 287 | 288 | The implementation looks a little hacky for maintainability it could 289 | be moved into its own Factory which creates this kind of response. 290 | 291 | Example: 292 | 293 | ```php 294 | return StreamedResponseFactory::create( 295 | [ 296 | 'embedded' => [ 297 | 'articles' => ['__REPLACES_ARTICLES__'], 298 | ], 299 | 'total' => 100_000, 300 | ], 301 | ['____REPLACES_ARTICLES__' => $articles] 302 | ); 303 | ``` 304 | 305 | The JavaScript part something is definitely not ready for production 306 | and if used you should probably creating your own content-type e.g.: 307 | `application/json+stream`. So you are parsing the json this way 308 | only when you know it is really in this line by line format. 309 | There maybe better libraries like [`JSONStream`](https://www.npmjs.com/package/JSONStream) 310 | to read data but at current state did test them out. Let me know 311 | if somebody has experience with that and has solutions for it. 312 | 313 | Atleast what I think everybody should use for providing lists 314 | is to use [`toIterable`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/reference/batch-processing.html#iterating-results) when possible for your lists when loading 315 | your data via Doctrine and and select specific fields instead 316 | of using the `ORM` to avoid hydration process to object. 317 | 318 | Let me know what you think about this experiment and how you currently are 319 | providing your JSON data. 320 | 321 | The whole experiment here can be checked out and test yourself via [this repository](https://github.com/alexander-schranz/efficient-json-streaming-with-symfony-doctrine). 322 | 323 | Attend the discussion about this on [Twitter](https://twitter.com/alex_s_/status/1488314080381313025). 324 | 325 | ## Update 2022-09-27 326 | 327 | Added a [StreamedJsonRepsonse](src/Controller/StreamedJsonResponse.php) class and 328 | try to contribute this implementation to the Symfony core. 329 | 330 | [https://github.com/symfony/symfony/pull/47709](https://github.com/symfony/symfony/pull/47709) 331 | 332 | ## Update 2022-10-02 333 | 334 | Updated some statistics with new machine and apache benchmark tests for concurrency requests. 335 | 336 | ## Update 202402-10 337 | 338 | Laravel is adopting the implementation and using the `StreamedJsonResponse` for there new `streamJson` method, 339 | thx to [pelmered](https://github.com/pelmered): 340 | [https://github.com/laravel/framework/pull/49873](https://github.com/laravel/framework/pull/49873). 341 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexanderschranz/stream-json-endpoint", 3 | "description": "A example to stream json response data.", 4 | "require": { 5 | "php": "^8.1", 6 | "doctrine/orm": "^2.11", 7 | "symfony/http-foundation": "^6.1", 8 | "doctrine/dbal": "^3.3", 9 | "symfony/cache": "^6.1" 10 | }, 11 | "license": "MIT", 12 | "autoload": { 13 | "psr-4": { 14 | "App\\": "src/" 15 | } 16 | }, 17 | "scripts": { 18 | "start": "" 19 | }, 20 | "authors": [ 21 | { 22 | "name": "Alexander Schranz", 23 | "homepage": "https://github.com/alexander-schranz/" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "c5d7331cacb00cd0ad0af66f6f09fc92", 8 | "packages": [ 9 | { 10 | "name": "doctrine/cache", 11 | "version": "2.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/doctrine/cache.git", 15 | "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", 20 | "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "~7.1 || ^8.0" 25 | }, 26 | "conflict": { 27 | "doctrine/common": ">2.2,<2.4" 28 | }, 29 | "require-dev": { 30 | "cache/integration-tests": "dev-master", 31 | "doctrine/coding-standard": "^9", 32 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 33 | "psr/cache": "^1.0 || ^2.0 || ^3.0", 34 | "symfony/cache": "^4.4 || ^5.4 || ^6", 35 | "symfony/var-exporter": "^4.4 || ^5.4 || ^6" 36 | }, 37 | "type": "library", 38 | "autoload": { 39 | "psr-4": { 40 | "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Guilherme Blanco", 50 | "email": "guilhermeblanco@gmail.com" 51 | }, 52 | { 53 | "name": "Roman Borschel", 54 | "email": "roman@code-factory.org" 55 | }, 56 | { 57 | "name": "Benjamin Eberlei", 58 | "email": "kontakt@beberlei.de" 59 | }, 60 | { 61 | "name": "Jonathan Wage", 62 | "email": "jonwage@gmail.com" 63 | }, 64 | { 65 | "name": "Johannes Schmitt", 66 | "email": "schmittjoh@gmail.com" 67 | } 68 | ], 69 | "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", 70 | "homepage": "https://www.doctrine-project.org/projects/cache.html", 71 | "keywords": [ 72 | "abstraction", 73 | "apcu", 74 | "cache", 75 | "caching", 76 | "couchdb", 77 | "memcached", 78 | "php", 79 | "redis", 80 | "xcache" 81 | ], 82 | "support": { 83 | "issues": "https://github.com/doctrine/cache/issues", 84 | "source": "https://github.com/doctrine/cache/tree/2.2.0" 85 | }, 86 | "funding": [ 87 | { 88 | "url": "https://www.doctrine-project.org/sponsorship.html", 89 | "type": "custom" 90 | }, 91 | { 92 | "url": "https://www.patreon.com/phpdoctrine", 93 | "type": "patreon" 94 | }, 95 | { 96 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", 97 | "type": "tidelift" 98 | } 99 | ], 100 | "time": "2022-05-20T20:07:39+00:00" 101 | }, 102 | { 103 | "name": "doctrine/collections", 104 | "version": "1.7.3", 105 | "source": { 106 | "type": "git", 107 | "url": "https://github.com/doctrine/collections.git", 108 | "reference": "09dde3eb237756190f2de738d3c97cff10a8407b" 109 | }, 110 | "dist": { 111 | "type": "zip", 112 | "url": "https://api.github.com/repos/doctrine/collections/zipball/09dde3eb237756190f2de738d3c97cff10a8407b", 113 | "reference": "09dde3eb237756190f2de738d3c97cff10a8407b", 114 | "shasum": "" 115 | }, 116 | "require": { 117 | "doctrine/deprecations": "^0.5.3 || ^1", 118 | "php": "^7.1.3 || ^8.0" 119 | }, 120 | "require-dev": { 121 | "doctrine/coding-standard": "^9.0 || ^10.0", 122 | "phpstan/phpstan": "^1.4.8", 123 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", 124 | "vimeo/psalm": "^4.22" 125 | }, 126 | "type": "library", 127 | "autoload": { 128 | "psr-4": { 129 | "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" 130 | } 131 | }, 132 | "notification-url": "https://packagist.org/downloads/", 133 | "license": [ 134 | "MIT" 135 | ], 136 | "authors": [ 137 | { 138 | "name": "Guilherme Blanco", 139 | "email": "guilhermeblanco@gmail.com" 140 | }, 141 | { 142 | "name": "Roman Borschel", 143 | "email": "roman@code-factory.org" 144 | }, 145 | { 146 | "name": "Benjamin Eberlei", 147 | "email": "kontakt@beberlei.de" 148 | }, 149 | { 150 | "name": "Jonathan Wage", 151 | "email": "jonwage@gmail.com" 152 | }, 153 | { 154 | "name": "Johannes Schmitt", 155 | "email": "schmittjoh@gmail.com" 156 | } 157 | ], 158 | "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", 159 | "homepage": "https://www.doctrine-project.org/projects/collections.html", 160 | "keywords": [ 161 | "array", 162 | "collections", 163 | "iterators", 164 | "php" 165 | ], 166 | "support": { 167 | "issues": "https://github.com/doctrine/collections/issues", 168 | "source": "https://github.com/doctrine/collections/tree/1.7.3" 169 | }, 170 | "time": "2022-09-01T19:34:23+00:00" 171 | }, 172 | { 173 | "name": "doctrine/common", 174 | "version": "3.4.1", 175 | "source": { 176 | "type": "git", 177 | "url": "https://github.com/doctrine/common.git", 178 | "reference": "616d5fca274ea66b969cdebbbfd1dc33a87d461b" 179 | }, 180 | "dist": { 181 | "type": "zip", 182 | "url": "https://api.github.com/repos/doctrine/common/zipball/616d5fca274ea66b969cdebbbfd1dc33a87d461b", 183 | "reference": "616d5fca274ea66b969cdebbbfd1dc33a87d461b", 184 | "shasum": "" 185 | }, 186 | "require": { 187 | "doctrine/persistence": "^2.0 || ^3.0", 188 | "php": "^7.1 || ^8.0" 189 | }, 190 | "require-dev": { 191 | "doctrine/coding-standard": "^9.0 || ^10.0", 192 | "doctrine/collections": "^1", 193 | "phpstan/phpstan": "^1.4.1", 194 | "phpstan/phpstan-phpunit": "^1", 195 | "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", 196 | "squizlabs/php_codesniffer": "^3.0", 197 | "symfony/phpunit-bridge": "^6.1", 198 | "vimeo/psalm": "^4.4" 199 | }, 200 | "type": "library", 201 | "autoload": { 202 | "psr-4": { 203 | "Doctrine\\Common\\": "src" 204 | } 205 | }, 206 | "notification-url": "https://packagist.org/downloads/", 207 | "license": [ 208 | "MIT" 209 | ], 210 | "authors": [ 211 | { 212 | "name": "Guilherme Blanco", 213 | "email": "guilhermeblanco@gmail.com" 214 | }, 215 | { 216 | "name": "Roman Borschel", 217 | "email": "roman@code-factory.org" 218 | }, 219 | { 220 | "name": "Benjamin Eberlei", 221 | "email": "kontakt@beberlei.de" 222 | }, 223 | { 224 | "name": "Jonathan Wage", 225 | "email": "jonwage@gmail.com" 226 | }, 227 | { 228 | "name": "Johannes Schmitt", 229 | "email": "schmittjoh@gmail.com" 230 | }, 231 | { 232 | "name": "Marco Pivetta", 233 | "email": "ocramius@gmail.com" 234 | } 235 | ], 236 | "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", 237 | "homepage": "https://www.doctrine-project.org/projects/common.html", 238 | "keywords": [ 239 | "common", 240 | "doctrine", 241 | "php" 242 | ], 243 | "support": { 244 | "issues": "https://github.com/doctrine/common/issues", 245 | "source": "https://github.com/doctrine/common/tree/3.4.1" 246 | }, 247 | "funding": [ 248 | { 249 | "url": "https://www.doctrine-project.org/sponsorship.html", 250 | "type": "custom" 251 | }, 252 | { 253 | "url": "https://www.patreon.com/phpdoctrine", 254 | "type": "patreon" 255 | }, 256 | { 257 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", 258 | "type": "tidelift" 259 | } 260 | ], 261 | "time": "2022-09-26T17:15:48+00:00" 262 | }, 263 | { 264 | "name": "doctrine/dbal", 265 | "version": "3.4.5", 266 | "source": { 267 | "type": "git", 268 | "url": "https://github.com/doctrine/dbal.git", 269 | "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e" 270 | }, 271 | "dist": { 272 | "type": "zip", 273 | "url": "https://api.github.com/repos/doctrine/dbal/zipball/a5a58773109c0abb13e658c8ccd92aeec8d07f9e", 274 | "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e", 275 | "shasum": "" 276 | }, 277 | "require": { 278 | "composer-runtime-api": "^2", 279 | "doctrine/cache": "^1.11|^2.0", 280 | "doctrine/deprecations": "^0.5.3|^1", 281 | "doctrine/event-manager": "^1.0", 282 | "php": "^7.4 || ^8.0", 283 | "psr/cache": "^1|^2|^3", 284 | "psr/log": "^1|^2|^3" 285 | }, 286 | "require-dev": { 287 | "doctrine/coding-standard": "10.0.0", 288 | "jetbrains/phpstorm-stubs": "2022.2", 289 | "phpstan/phpstan": "1.8.3", 290 | "phpstan/phpstan-strict-rules": "^1.3", 291 | "phpunit/phpunit": "9.5.24", 292 | "psalm/plugin-phpunit": "0.17.0", 293 | "squizlabs/php_codesniffer": "3.7.1", 294 | "symfony/cache": "^5.4|^6.0", 295 | "symfony/console": "^4.4|^5.4|^6.0", 296 | "vimeo/psalm": "4.27.0" 297 | }, 298 | "suggest": { 299 | "symfony/console": "For helpful console commands such as SQL execution and import of files." 300 | }, 301 | "bin": [ 302 | "bin/doctrine-dbal" 303 | ], 304 | "type": "library", 305 | "autoload": { 306 | "psr-4": { 307 | "Doctrine\\DBAL\\": "src" 308 | } 309 | }, 310 | "notification-url": "https://packagist.org/downloads/", 311 | "license": [ 312 | "MIT" 313 | ], 314 | "authors": [ 315 | { 316 | "name": "Guilherme Blanco", 317 | "email": "guilhermeblanco@gmail.com" 318 | }, 319 | { 320 | "name": "Roman Borschel", 321 | "email": "roman@code-factory.org" 322 | }, 323 | { 324 | "name": "Benjamin Eberlei", 325 | "email": "kontakt@beberlei.de" 326 | }, 327 | { 328 | "name": "Jonathan Wage", 329 | "email": "jonwage@gmail.com" 330 | } 331 | ], 332 | "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", 333 | "homepage": "https://www.doctrine-project.org/projects/dbal.html", 334 | "keywords": [ 335 | "abstraction", 336 | "database", 337 | "db2", 338 | "dbal", 339 | "mariadb", 340 | "mssql", 341 | "mysql", 342 | "oci8", 343 | "oracle", 344 | "pdo", 345 | "pgsql", 346 | "postgresql", 347 | "queryobject", 348 | "sasql", 349 | "sql", 350 | "sqlite", 351 | "sqlserver", 352 | "sqlsrv" 353 | ], 354 | "support": { 355 | "issues": "https://github.com/doctrine/dbal/issues", 356 | "source": "https://github.com/doctrine/dbal/tree/3.4.5" 357 | }, 358 | "funding": [ 359 | { 360 | "url": "https://www.doctrine-project.org/sponsorship.html", 361 | "type": "custom" 362 | }, 363 | { 364 | "url": "https://www.patreon.com/phpdoctrine", 365 | "type": "patreon" 366 | }, 367 | { 368 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", 369 | "type": "tidelift" 370 | } 371 | ], 372 | "time": "2022-09-23T17:48:57+00:00" 373 | }, 374 | { 375 | "name": "doctrine/deprecations", 376 | "version": "v1.0.0", 377 | "source": { 378 | "type": "git", 379 | "url": "https://github.com/doctrine/deprecations.git", 380 | "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" 381 | }, 382 | "dist": { 383 | "type": "zip", 384 | "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", 385 | "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", 386 | "shasum": "" 387 | }, 388 | "require": { 389 | "php": "^7.1|^8.0" 390 | }, 391 | "require-dev": { 392 | "doctrine/coding-standard": "^9", 393 | "phpunit/phpunit": "^7.5|^8.5|^9.5", 394 | "psr/log": "^1|^2|^3" 395 | }, 396 | "suggest": { 397 | "psr/log": "Allows logging deprecations via PSR-3 logger implementation" 398 | }, 399 | "type": "library", 400 | "autoload": { 401 | "psr-4": { 402 | "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" 403 | } 404 | }, 405 | "notification-url": "https://packagist.org/downloads/", 406 | "license": [ 407 | "MIT" 408 | ], 409 | "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", 410 | "homepage": "https://www.doctrine-project.org/", 411 | "support": { 412 | "issues": "https://github.com/doctrine/deprecations/issues", 413 | "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" 414 | }, 415 | "time": "2022-05-02T15:47:09+00:00" 416 | }, 417 | { 418 | "name": "doctrine/event-manager", 419 | "version": "1.1.2", 420 | "source": { 421 | "type": "git", 422 | "url": "https://github.com/doctrine/event-manager.git", 423 | "reference": "eb2ecf80e3093e8f3c2769ac838e27d8ede8e683" 424 | }, 425 | "dist": { 426 | "type": "zip", 427 | "url": "https://api.github.com/repos/doctrine/event-manager/zipball/eb2ecf80e3093e8f3c2769ac838e27d8ede8e683", 428 | "reference": "eb2ecf80e3093e8f3c2769ac838e27d8ede8e683", 429 | "shasum": "" 430 | }, 431 | "require": { 432 | "php": "^7.1 || ^8.0" 433 | }, 434 | "conflict": { 435 | "doctrine/common": "<2.9" 436 | }, 437 | "require-dev": { 438 | "doctrine/coding-standard": "^9", 439 | "phpstan/phpstan": "~1.4.10 || ^1.5.4", 440 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 441 | "vimeo/psalm": "^4.22" 442 | }, 443 | "type": "library", 444 | "autoload": { 445 | "psr-4": { 446 | "Doctrine\\Common\\": "lib/Doctrine/Common" 447 | } 448 | }, 449 | "notification-url": "https://packagist.org/downloads/", 450 | "license": [ 451 | "MIT" 452 | ], 453 | "authors": [ 454 | { 455 | "name": "Guilherme Blanco", 456 | "email": "guilhermeblanco@gmail.com" 457 | }, 458 | { 459 | "name": "Roman Borschel", 460 | "email": "roman@code-factory.org" 461 | }, 462 | { 463 | "name": "Benjamin Eberlei", 464 | "email": "kontakt@beberlei.de" 465 | }, 466 | { 467 | "name": "Jonathan Wage", 468 | "email": "jonwage@gmail.com" 469 | }, 470 | { 471 | "name": "Johannes Schmitt", 472 | "email": "schmittjoh@gmail.com" 473 | }, 474 | { 475 | "name": "Marco Pivetta", 476 | "email": "ocramius@gmail.com" 477 | } 478 | ], 479 | "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", 480 | "homepage": "https://www.doctrine-project.org/projects/event-manager.html", 481 | "keywords": [ 482 | "event", 483 | "event dispatcher", 484 | "event manager", 485 | "event system", 486 | "events" 487 | ], 488 | "support": { 489 | "issues": "https://github.com/doctrine/event-manager/issues", 490 | "source": "https://github.com/doctrine/event-manager/tree/1.1.2" 491 | }, 492 | "funding": [ 493 | { 494 | "url": "https://www.doctrine-project.org/sponsorship.html", 495 | "type": "custom" 496 | }, 497 | { 498 | "url": "https://www.patreon.com/phpdoctrine", 499 | "type": "patreon" 500 | }, 501 | { 502 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", 503 | "type": "tidelift" 504 | } 505 | ], 506 | "time": "2022-07-27T22:18:11+00:00" 507 | }, 508 | { 509 | "name": "doctrine/inflector", 510 | "version": "2.0.5", 511 | "source": { 512 | "type": "git", 513 | "url": "https://github.com/doctrine/inflector.git", 514 | "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392" 515 | }, 516 | "dist": { 517 | "type": "zip", 518 | "url": "https://api.github.com/repos/doctrine/inflector/zipball/ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", 519 | "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", 520 | "shasum": "" 521 | }, 522 | "require": { 523 | "php": "^7.2 || ^8.0" 524 | }, 525 | "require-dev": { 526 | "doctrine/coding-standard": "^9", 527 | "phpstan/phpstan": "^1.8", 528 | "phpstan/phpstan-phpunit": "^1.1", 529 | "phpstan/phpstan-strict-rules": "^1.3", 530 | "phpunit/phpunit": "^8.5 || ^9.5", 531 | "vimeo/psalm": "^4.25" 532 | }, 533 | "type": "library", 534 | "autoload": { 535 | "psr-4": { 536 | "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" 537 | } 538 | }, 539 | "notification-url": "https://packagist.org/downloads/", 540 | "license": [ 541 | "MIT" 542 | ], 543 | "authors": [ 544 | { 545 | "name": "Guilherme Blanco", 546 | "email": "guilhermeblanco@gmail.com" 547 | }, 548 | { 549 | "name": "Roman Borschel", 550 | "email": "roman@code-factory.org" 551 | }, 552 | { 553 | "name": "Benjamin Eberlei", 554 | "email": "kontakt@beberlei.de" 555 | }, 556 | { 557 | "name": "Jonathan Wage", 558 | "email": "jonwage@gmail.com" 559 | }, 560 | { 561 | "name": "Johannes Schmitt", 562 | "email": "schmittjoh@gmail.com" 563 | } 564 | ], 565 | "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", 566 | "homepage": "https://www.doctrine-project.org/projects/inflector.html", 567 | "keywords": [ 568 | "inflection", 569 | "inflector", 570 | "lowercase", 571 | "manipulation", 572 | "php", 573 | "plural", 574 | "singular", 575 | "strings", 576 | "uppercase", 577 | "words" 578 | ], 579 | "support": { 580 | "issues": "https://github.com/doctrine/inflector/issues", 581 | "source": "https://github.com/doctrine/inflector/tree/2.0.5" 582 | }, 583 | "funding": [ 584 | { 585 | "url": "https://www.doctrine-project.org/sponsorship.html", 586 | "type": "custom" 587 | }, 588 | { 589 | "url": "https://www.patreon.com/phpdoctrine", 590 | "type": "patreon" 591 | }, 592 | { 593 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", 594 | "type": "tidelift" 595 | } 596 | ], 597 | "time": "2022-09-07T09:01:28+00:00" 598 | }, 599 | { 600 | "name": "doctrine/instantiator", 601 | "version": "1.4.1", 602 | "source": { 603 | "type": "git", 604 | "url": "https://github.com/doctrine/instantiator.git", 605 | "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" 606 | }, 607 | "dist": { 608 | "type": "zip", 609 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", 610 | "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", 611 | "shasum": "" 612 | }, 613 | "require": { 614 | "php": "^7.1 || ^8.0" 615 | }, 616 | "require-dev": { 617 | "doctrine/coding-standard": "^9", 618 | "ext-pdo": "*", 619 | "ext-phar": "*", 620 | "phpbench/phpbench": "^0.16 || ^1", 621 | "phpstan/phpstan": "^1.4", 622 | "phpstan/phpstan-phpunit": "^1", 623 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 624 | "vimeo/psalm": "^4.22" 625 | }, 626 | "type": "library", 627 | "autoload": { 628 | "psr-4": { 629 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 630 | } 631 | }, 632 | "notification-url": "https://packagist.org/downloads/", 633 | "license": [ 634 | "MIT" 635 | ], 636 | "authors": [ 637 | { 638 | "name": "Marco Pivetta", 639 | "email": "ocramius@gmail.com", 640 | "homepage": "https://ocramius.github.io/" 641 | } 642 | ], 643 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 644 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 645 | "keywords": [ 646 | "constructor", 647 | "instantiate" 648 | ], 649 | "support": { 650 | "issues": "https://github.com/doctrine/instantiator/issues", 651 | "source": "https://github.com/doctrine/instantiator/tree/1.4.1" 652 | }, 653 | "funding": [ 654 | { 655 | "url": "https://www.doctrine-project.org/sponsorship.html", 656 | "type": "custom" 657 | }, 658 | { 659 | "url": "https://www.patreon.com/phpdoctrine", 660 | "type": "patreon" 661 | }, 662 | { 663 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 664 | "type": "tidelift" 665 | } 666 | ], 667 | "time": "2022-03-03T08:28:38+00:00" 668 | }, 669 | { 670 | "name": "doctrine/lexer", 671 | "version": "1.2.3", 672 | "source": { 673 | "type": "git", 674 | "url": "https://github.com/doctrine/lexer.git", 675 | "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" 676 | }, 677 | "dist": { 678 | "type": "zip", 679 | "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", 680 | "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", 681 | "shasum": "" 682 | }, 683 | "require": { 684 | "php": "^7.1 || ^8.0" 685 | }, 686 | "require-dev": { 687 | "doctrine/coding-standard": "^9.0", 688 | "phpstan/phpstan": "^1.3", 689 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 690 | "vimeo/psalm": "^4.11" 691 | }, 692 | "type": "library", 693 | "autoload": { 694 | "psr-4": { 695 | "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" 696 | } 697 | }, 698 | "notification-url": "https://packagist.org/downloads/", 699 | "license": [ 700 | "MIT" 701 | ], 702 | "authors": [ 703 | { 704 | "name": "Guilherme Blanco", 705 | "email": "guilhermeblanco@gmail.com" 706 | }, 707 | { 708 | "name": "Roman Borschel", 709 | "email": "roman@code-factory.org" 710 | }, 711 | { 712 | "name": "Johannes Schmitt", 713 | "email": "schmittjoh@gmail.com" 714 | } 715 | ], 716 | "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", 717 | "homepage": "https://www.doctrine-project.org/projects/lexer.html", 718 | "keywords": [ 719 | "annotations", 720 | "docblock", 721 | "lexer", 722 | "parser", 723 | "php" 724 | ], 725 | "support": { 726 | "issues": "https://github.com/doctrine/lexer/issues", 727 | "source": "https://github.com/doctrine/lexer/tree/1.2.3" 728 | }, 729 | "funding": [ 730 | { 731 | "url": "https://www.doctrine-project.org/sponsorship.html", 732 | "type": "custom" 733 | }, 734 | { 735 | "url": "https://www.patreon.com/phpdoctrine", 736 | "type": "patreon" 737 | }, 738 | { 739 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", 740 | "type": "tidelift" 741 | } 742 | ], 743 | "time": "2022-02-28T11:07:21+00:00" 744 | }, 745 | { 746 | "name": "doctrine/orm", 747 | "version": "2.13.2", 748 | "source": { 749 | "type": "git", 750 | "url": "https://github.com/doctrine/orm.git", 751 | "reference": "a8b02fd70fa777ca8278b9604fdef75c15c6a12f" 752 | }, 753 | "dist": { 754 | "type": "zip", 755 | "url": "https://api.github.com/repos/doctrine/orm/zipball/a8b02fd70fa777ca8278b9604fdef75c15c6a12f", 756 | "reference": "a8b02fd70fa777ca8278b9604fdef75c15c6a12f", 757 | "shasum": "" 758 | }, 759 | "require": { 760 | "composer-runtime-api": "^2", 761 | "doctrine/cache": "^1.12.1 || ^2.1.1", 762 | "doctrine/collections": "^1.5", 763 | "doctrine/common": "^3.0.3", 764 | "doctrine/dbal": "^2.13.1 || ^3.2", 765 | "doctrine/deprecations": "^0.5.3 || ^1", 766 | "doctrine/event-manager": "^1.1", 767 | "doctrine/inflector": "^1.4 || ^2.0", 768 | "doctrine/instantiator": "^1.3", 769 | "doctrine/lexer": "^1.2.3", 770 | "doctrine/persistence": "^2.4 || ^3", 771 | "ext-ctype": "*", 772 | "php": "^7.1 || ^8.0", 773 | "psr/cache": "^1 || ^2 || ^3", 774 | "symfony/console": "^3.0 || ^4.0 || ^5.0 || ^6.0", 775 | "symfony/polyfill-php72": "^1.23", 776 | "symfony/polyfill-php80": "^1.16" 777 | }, 778 | "conflict": { 779 | "doctrine/annotations": "<1.13 || >= 2.0" 780 | }, 781 | "require-dev": { 782 | "doctrine/annotations": "^1.13", 783 | "doctrine/coding-standard": "^9.0.2 || ^10.0", 784 | "phpbench/phpbench": "^0.16.10 || ^1.0", 785 | "phpstan/phpstan": "~1.4.10 || 1.8.5", 786 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 787 | "psr/log": "^1 || ^2 || ^3", 788 | "squizlabs/php_codesniffer": "3.7.1", 789 | "symfony/cache": "^4.4 || ^5.4 || ^6.0", 790 | "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", 791 | "vimeo/psalm": "4.27.0" 792 | }, 793 | "suggest": { 794 | "ext-dom": "Provides support for XSD validation for XML mapping files", 795 | "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", 796 | "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" 797 | }, 798 | "bin": [ 799 | "bin/doctrine" 800 | ], 801 | "type": "library", 802 | "autoload": { 803 | "psr-4": { 804 | "Doctrine\\ORM\\": "lib/Doctrine/ORM" 805 | } 806 | }, 807 | "notification-url": "https://packagist.org/downloads/", 808 | "license": [ 809 | "MIT" 810 | ], 811 | "authors": [ 812 | { 813 | "name": "Guilherme Blanco", 814 | "email": "guilhermeblanco@gmail.com" 815 | }, 816 | { 817 | "name": "Roman Borschel", 818 | "email": "roman@code-factory.org" 819 | }, 820 | { 821 | "name": "Benjamin Eberlei", 822 | "email": "kontakt@beberlei.de" 823 | }, 824 | { 825 | "name": "Jonathan Wage", 826 | "email": "jonwage@gmail.com" 827 | }, 828 | { 829 | "name": "Marco Pivetta", 830 | "email": "ocramius@gmail.com" 831 | } 832 | ], 833 | "description": "Object-Relational-Mapper for PHP", 834 | "homepage": "https://www.doctrine-project.org/projects/orm.html", 835 | "keywords": [ 836 | "database", 837 | "orm" 838 | ], 839 | "support": { 840 | "issues": "https://github.com/doctrine/orm/issues", 841 | "source": "https://github.com/doctrine/orm/tree/2.13.2" 842 | }, 843 | "time": "2022-09-22T13:36:43+00:00" 844 | }, 845 | { 846 | "name": "doctrine/persistence", 847 | "version": "3.0.3", 848 | "source": { 849 | "type": "git", 850 | "url": "https://github.com/doctrine/persistence.git", 851 | "reference": "ac6fce61f037d7e54dbb2435f5b5648d86548e23" 852 | }, 853 | "dist": { 854 | "type": "zip", 855 | "url": "https://api.github.com/repos/doctrine/persistence/zipball/ac6fce61f037d7e54dbb2435f5b5648d86548e23", 856 | "reference": "ac6fce61f037d7e54dbb2435f5b5648d86548e23", 857 | "shasum": "" 858 | }, 859 | "require": { 860 | "doctrine/event-manager": "^1.0", 861 | "php": "^7.2 || ^8.0", 862 | "psr/cache": "^1.0 || ^2.0 || ^3.0" 863 | }, 864 | "conflict": { 865 | "doctrine/annotations": "<1.7 || >=2.0", 866 | "doctrine/common": "<2.10" 867 | }, 868 | "require-dev": { 869 | "composer/package-versions-deprecated": "^1.11", 870 | "doctrine/annotations": "^1.7", 871 | "doctrine/coding-standard": "^9.0", 872 | "doctrine/common": "^3.0", 873 | "phpstan/phpstan": "1.5.0", 874 | "phpstan/phpstan-phpunit": "^1", 875 | "phpstan/phpstan-strict-rules": "^1.1", 876 | "phpunit/phpunit": "^8.5 || ^9.5", 877 | "symfony/cache": "^4.4 || ^5.4 || ^6.0", 878 | "vimeo/psalm": "4.22.0" 879 | }, 880 | "type": "library", 881 | "autoload": { 882 | "psr-4": { 883 | "Doctrine\\Persistence\\": "src/Persistence" 884 | } 885 | }, 886 | "notification-url": "https://packagist.org/downloads/", 887 | "license": [ 888 | "MIT" 889 | ], 890 | "authors": [ 891 | { 892 | "name": "Guilherme Blanco", 893 | "email": "guilhermeblanco@gmail.com" 894 | }, 895 | { 896 | "name": "Roman Borschel", 897 | "email": "roman@code-factory.org" 898 | }, 899 | { 900 | "name": "Benjamin Eberlei", 901 | "email": "kontakt@beberlei.de" 902 | }, 903 | { 904 | "name": "Jonathan Wage", 905 | "email": "jonwage@gmail.com" 906 | }, 907 | { 908 | "name": "Johannes Schmitt", 909 | "email": "schmittjoh@gmail.com" 910 | }, 911 | { 912 | "name": "Marco Pivetta", 913 | "email": "ocramius@gmail.com" 914 | } 915 | ], 916 | "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", 917 | "homepage": "https://www.doctrine-project.org/projects/persistence.html", 918 | "keywords": [ 919 | "mapper", 920 | "object", 921 | "odm", 922 | "orm", 923 | "persistence" 924 | ], 925 | "support": { 926 | "issues": "https://github.com/doctrine/persistence/issues", 927 | "source": "https://github.com/doctrine/persistence/tree/3.0.3" 928 | }, 929 | "funding": [ 930 | { 931 | "url": "https://www.doctrine-project.org/sponsorship.html", 932 | "type": "custom" 933 | }, 934 | { 935 | "url": "https://www.patreon.com/phpdoctrine", 936 | "type": "patreon" 937 | }, 938 | { 939 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence", 940 | "type": "tidelift" 941 | } 942 | ], 943 | "time": "2022-08-04T21:14:21+00:00" 944 | }, 945 | { 946 | "name": "psr/cache", 947 | "version": "3.0.0", 948 | "source": { 949 | "type": "git", 950 | "url": "https://github.com/php-fig/cache.git", 951 | "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" 952 | }, 953 | "dist": { 954 | "type": "zip", 955 | "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", 956 | "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", 957 | "shasum": "" 958 | }, 959 | "require": { 960 | "php": ">=8.0.0" 961 | }, 962 | "type": "library", 963 | "extra": { 964 | "branch-alias": { 965 | "dev-master": "1.0.x-dev" 966 | } 967 | }, 968 | "autoload": { 969 | "psr-4": { 970 | "Psr\\Cache\\": "src/" 971 | } 972 | }, 973 | "notification-url": "https://packagist.org/downloads/", 974 | "license": [ 975 | "MIT" 976 | ], 977 | "authors": [ 978 | { 979 | "name": "PHP-FIG", 980 | "homepage": "https://www.php-fig.org/" 981 | } 982 | ], 983 | "description": "Common interface for caching libraries", 984 | "keywords": [ 985 | "cache", 986 | "psr", 987 | "psr-6" 988 | ], 989 | "support": { 990 | "source": "https://github.com/php-fig/cache/tree/3.0.0" 991 | }, 992 | "time": "2021-02-03T23:26:27+00:00" 993 | }, 994 | { 995 | "name": "psr/container", 996 | "version": "2.0.2", 997 | "source": { 998 | "type": "git", 999 | "url": "https://github.com/php-fig/container.git", 1000 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" 1001 | }, 1002 | "dist": { 1003 | "type": "zip", 1004 | "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", 1005 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", 1006 | "shasum": "" 1007 | }, 1008 | "require": { 1009 | "php": ">=7.4.0" 1010 | }, 1011 | "type": "library", 1012 | "extra": { 1013 | "branch-alias": { 1014 | "dev-master": "2.0.x-dev" 1015 | } 1016 | }, 1017 | "autoload": { 1018 | "psr-4": { 1019 | "Psr\\Container\\": "src/" 1020 | } 1021 | }, 1022 | "notification-url": "https://packagist.org/downloads/", 1023 | "license": [ 1024 | "MIT" 1025 | ], 1026 | "authors": [ 1027 | { 1028 | "name": "PHP-FIG", 1029 | "homepage": "https://www.php-fig.org/" 1030 | } 1031 | ], 1032 | "description": "Common Container Interface (PHP FIG PSR-11)", 1033 | "homepage": "https://github.com/php-fig/container", 1034 | "keywords": [ 1035 | "PSR-11", 1036 | "container", 1037 | "container-interface", 1038 | "container-interop", 1039 | "psr" 1040 | ], 1041 | "support": { 1042 | "issues": "https://github.com/php-fig/container/issues", 1043 | "source": "https://github.com/php-fig/container/tree/2.0.2" 1044 | }, 1045 | "time": "2021-11-05T16:47:00+00:00" 1046 | }, 1047 | { 1048 | "name": "psr/log", 1049 | "version": "3.0.0", 1050 | "source": { 1051 | "type": "git", 1052 | "url": "https://github.com/php-fig/log.git", 1053 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" 1054 | }, 1055 | "dist": { 1056 | "type": "zip", 1057 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", 1058 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", 1059 | "shasum": "" 1060 | }, 1061 | "require": { 1062 | "php": ">=8.0.0" 1063 | }, 1064 | "type": "library", 1065 | "extra": { 1066 | "branch-alias": { 1067 | "dev-master": "3.x-dev" 1068 | } 1069 | }, 1070 | "autoload": { 1071 | "psr-4": { 1072 | "Psr\\Log\\": "src" 1073 | } 1074 | }, 1075 | "notification-url": "https://packagist.org/downloads/", 1076 | "license": [ 1077 | "MIT" 1078 | ], 1079 | "authors": [ 1080 | { 1081 | "name": "PHP-FIG", 1082 | "homepage": "https://www.php-fig.org/" 1083 | } 1084 | ], 1085 | "description": "Common interface for logging libraries", 1086 | "homepage": "https://github.com/php-fig/log", 1087 | "keywords": [ 1088 | "log", 1089 | "psr", 1090 | "psr-3" 1091 | ], 1092 | "support": { 1093 | "source": "https://github.com/php-fig/log/tree/3.0.0" 1094 | }, 1095 | "time": "2021-07-14T16:46:02+00:00" 1096 | }, 1097 | { 1098 | "name": "symfony/cache", 1099 | "version": "v6.1.3", 1100 | "source": { 1101 | "type": "git", 1102 | "url": "https://github.com/symfony/cache.git", 1103 | "reference": "5cf8e75f02932818889e0609380b8d5427a6c86c" 1104 | }, 1105 | "dist": { 1106 | "type": "zip", 1107 | "url": "https://api.github.com/repos/symfony/cache/zipball/5cf8e75f02932818889e0609380b8d5427a6c86c", 1108 | "reference": "5cf8e75f02932818889e0609380b8d5427a6c86c", 1109 | "shasum": "" 1110 | }, 1111 | "require": { 1112 | "php": ">=8.1", 1113 | "psr/cache": "^2.0|^3.0", 1114 | "psr/log": "^1.1|^2|^3", 1115 | "symfony/cache-contracts": "^1.1.7|^2|^3", 1116 | "symfony/service-contracts": "^1.1|^2|^3", 1117 | "symfony/var-exporter": "^5.4|^6.0" 1118 | }, 1119 | "conflict": { 1120 | "doctrine/dbal": "<2.13.1", 1121 | "symfony/dependency-injection": "<5.4", 1122 | "symfony/http-kernel": "<5.4", 1123 | "symfony/var-dumper": "<5.4" 1124 | }, 1125 | "provide": { 1126 | "psr/cache-implementation": "2.0|3.0", 1127 | "psr/simple-cache-implementation": "1.0|2.0|3.0", 1128 | "symfony/cache-implementation": "1.1|2.0|3.0" 1129 | }, 1130 | "require-dev": { 1131 | "cache/integration-tests": "dev-master", 1132 | "doctrine/dbal": "^2.13.1|^3.0", 1133 | "predis/predis": "^1.1", 1134 | "psr/simple-cache": "^1.0|^2.0|^3.0", 1135 | "symfony/config": "^5.4|^6.0", 1136 | "symfony/dependency-injection": "^5.4|^6.0", 1137 | "symfony/filesystem": "^5.4|^6.0", 1138 | "symfony/http-kernel": "^5.4|^6.0", 1139 | "symfony/messenger": "^5.4|^6.0", 1140 | "symfony/var-dumper": "^5.4|^6.0" 1141 | }, 1142 | "type": "library", 1143 | "autoload": { 1144 | "psr-4": { 1145 | "Symfony\\Component\\Cache\\": "" 1146 | }, 1147 | "classmap": [ 1148 | "Traits/ValueWrapper.php" 1149 | ], 1150 | "exclude-from-classmap": [ 1151 | "/Tests/" 1152 | ] 1153 | }, 1154 | "notification-url": "https://packagist.org/downloads/", 1155 | "license": [ 1156 | "MIT" 1157 | ], 1158 | "authors": [ 1159 | { 1160 | "name": "Nicolas Grekas", 1161 | "email": "p@tchwork.com" 1162 | }, 1163 | { 1164 | "name": "Symfony Community", 1165 | "homepage": "https://symfony.com/contributors" 1166 | } 1167 | ], 1168 | "description": "Provides an extended PSR-6, PSR-16 (and tags) implementation", 1169 | "homepage": "https://symfony.com", 1170 | "keywords": [ 1171 | "caching", 1172 | "psr6" 1173 | ], 1174 | "support": { 1175 | "source": "https://github.com/symfony/cache/tree/v6.1.3" 1176 | }, 1177 | "funding": [ 1178 | { 1179 | "url": "https://symfony.com/sponsor", 1180 | "type": "custom" 1181 | }, 1182 | { 1183 | "url": "https://github.com/fabpot", 1184 | "type": "github" 1185 | }, 1186 | { 1187 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1188 | "type": "tidelift" 1189 | } 1190 | ], 1191 | "time": "2022-07-29T07:42:06+00:00" 1192 | }, 1193 | { 1194 | "name": "symfony/cache-contracts", 1195 | "version": "v3.1.1", 1196 | "source": { 1197 | "type": "git", 1198 | "url": "https://github.com/symfony/cache-contracts.git", 1199 | "reference": "2eab7fa459af6d75c6463e63e633b667a9b761d3" 1200 | }, 1201 | "dist": { 1202 | "type": "zip", 1203 | "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/2eab7fa459af6d75c6463e63e633b667a9b761d3", 1204 | "reference": "2eab7fa459af6d75c6463e63e633b667a9b761d3", 1205 | "shasum": "" 1206 | }, 1207 | "require": { 1208 | "php": ">=8.1", 1209 | "psr/cache": "^3.0" 1210 | }, 1211 | "suggest": { 1212 | "symfony/cache-implementation": "" 1213 | }, 1214 | "type": "library", 1215 | "extra": { 1216 | "branch-alias": { 1217 | "dev-main": "3.1-dev" 1218 | }, 1219 | "thanks": { 1220 | "name": "symfony/contracts", 1221 | "url": "https://github.com/symfony/contracts" 1222 | } 1223 | }, 1224 | "autoload": { 1225 | "psr-4": { 1226 | "Symfony\\Contracts\\Cache\\": "" 1227 | } 1228 | }, 1229 | "notification-url": "https://packagist.org/downloads/", 1230 | "license": [ 1231 | "MIT" 1232 | ], 1233 | "authors": [ 1234 | { 1235 | "name": "Nicolas Grekas", 1236 | "email": "p@tchwork.com" 1237 | }, 1238 | { 1239 | "name": "Symfony Community", 1240 | "homepage": "https://symfony.com/contributors" 1241 | } 1242 | ], 1243 | "description": "Generic abstractions related to caching", 1244 | "homepage": "https://symfony.com", 1245 | "keywords": [ 1246 | "abstractions", 1247 | "contracts", 1248 | "decoupling", 1249 | "interfaces", 1250 | "interoperability", 1251 | "standards" 1252 | ], 1253 | "support": { 1254 | "source": "https://github.com/symfony/cache-contracts/tree/v3.1.1" 1255 | }, 1256 | "funding": [ 1257 | { 1258 | "url": "https://symfony.com/sponsor", 1259 | "type": "custom" 1260 | }, 1261 | { 1262 | "url": "https://github.com/fabpot", 1263 | "type": "github" 1264 | }, 1265 | { 1266 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1267 | "type": "tidelift" 1268 | } 1269 | ], 1270 | "time": "2022-02-25T11:15:52+00:00" 1271 | }, 1272 | { 1273 | "name": "symfony/console", 1274 | "version": "v6.1.4", 1275 | "source": { 1276 | "type": "git", 1277 | "url": "https://github.com/symfony/console.git", 1278 | "reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d" 1279 | }, 1280 | "dist": { 1281 | "type": "zip", 1282 | "url": "https://api.github.com/repos/symfony/console/zipball/7fccea8728aa2d431a6725b02b3ce759049fc84d", 1283 | "reference": "7fccea8728aa2d431a6725b02b3ce759049fc84d", 1284 | "shasum": "" 1285 | }, 1286 | "require": { 1287 | "php": ">=8.1", 1288 | "symfony/deprecation-contracts": "^2.1|^3", 1289 | "symfony/polyfill-mbstring": "~1.0", 1290 | "symfony/service-contracts": "^1.1|^2|^3", 1291 | "symfony/string": "^5.4|^6.0" 1292 | }, 1293 | "conflict": { 1294 | "symfony/dependency-injection": "<5.4", 1295 | "symfony/dotenv": "<5.4", 1296 | "symfony/event-dispatcher": "<5.4", 1297 | "symfony/lock": "<5.4", 1298 | "symfony/process": "<5.4" 1299 | }, 1300 | "provide": { 1301 | "psr/log-implementation": "1.0|2.0|3.0" 1302 | }, 1303 | "require-dev": { 1304 | "psr/log": "^1|^2|^3", 1305 | "symfony/config": "^5.4|^6.0", 1306 | "symfony/dependency-injection": "^5.4|^6.0", 1307 | "symfony/event-dispatcher": "^5.4|^6.0", 1308 | "symfony/lock": "^5.4|^6.0", 1309 | "symfony/process": "^5.4|^6.0", 1310 | "symfony/var-dumper": "^5.4|^6.0" 1311 | }, 1312 | "suggest": { 1313 | "psr/log": "For using the console logger", 1314 | "symfony/event-dispatcher": "", 1315 | "symfony/lock": "", 1316 | "symfony/process": "" 1317 | }, 1318 | "type": "library", 1319 | "autoload": { 1320 | "psr-4": { 1321 | "Symfony\\Component\\Console\\": "" 1322 | }, 1323 | "exclude-from-classmap": [ 1324 | "/Tests/" 1325 | ] 1326 | }, 1327 | "notification-url": "https://packagist.org/downloads/", 1328 | "license": [ 1329 | "MIT" 1330 | ], 1331 | "authors": [ 1332 | { 1333 | "name": "Fabien Potencier", 1334 | "email": "fabien@symfony.com" 1335 | }, 1336 | { 1337 | "name": "Symfony Community", 1338 | "homepage": "https://symfony.com/contributors" 1339 | } 1340 | ], 1341 | "description": "Eases the creation of beautiful and testable command line interfaces", 1342 | "homepage": "https://symfony.com", 1343 | "keywords": [ 1344 | "cli", 1345 | "command line", 1346 | "console", 1347 | "terminal" 1348 | ], 1349 | "support": { 1350 | "source": "https://github.com/symfony/console/tree/v6.1.4" 1351 | }, 1352 | "funding": [ 1353 | { 1354 | "url": "https://symfony.com/sponsor", 1355 | "type": "custom" 1356 | }, 1357 | { 1358 | "url": "https://github.com/fabpot", 1359 | "type": "github" 1360 | }, 1361 | { 1362 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1363 | "type": "tidelift" 1364 | } 1365 | ], 1366 | "time": "2022-08-26T10:32:31+00:00" 1367 | }, 1368 | { 1369 | "name": "symfony/deprecation-contracts", 1370 | "version": "v3.1.1", 1371 | "source": { 1372 | "type": "git", 1373 | "url": "https://github.com/symfony/deprecation-contracts.git", 1374 | "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" 1375 | }, 1376 | "dist": { 1377 | "type": "zip", 1378 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", 1379 | "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", 1380 | "shasum": "" 1381 | }, 1382 | "require": { 1383 | "php": ">=8.1" 1384 | }, 1385 | "type": "library", 1386 | "extra": { 1387 | "branch-alias": { 1388 | "dev-main": "3.1-dev" 1389 | }, 1390 | "thanks": { 1391 | "name": "symfony/contracts", 1392 | "url": "https://github.com/symfony/contracts" 1393 | } 1394 | }, 1395 | "autoload": { 1396 | "files": [ 1397 | "function.php" 1398 | ] 1399 | }, 1400 | "notification-url": "https://packagist.org/downloads/", 1401 | "license": [ 1402 | "MIT" 1403 | ], 1404 | "authors": [ 1405 | { 1406 | "name": "Nicolas Grekas", 1407 | "email": "p@tchwork.com" 1408 | }, 1409 | { 1410 | "name": "Symfony Community", 1411 | "homepage": "https://symfony.com/contributors" 1412 | } 1413 | ], 1414 | "description": "A generic function and convention to trigger deprecation notices", 1415 | "homepage": "https://symfony.com", 1416 | "support": { 1417 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1" 1418 | }, 1419 | "funding": [ 1420 | { 1421 | "url": "https://symfony.com/sponsor", 1422 | "type": "custom" 1423 | }, 1424 | { 1425 | "url": "https://github.com/fabpot", 1426 | "type": "github" 1427 | }, 1428 | { 1429 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1430 | "type": "tidelift" 1431 | } 1432 | ], 1433 | "time": "2022-02-25T11:15:52+00:00" 1434 | }, 1435 | { 1436 | "name": "symfony/http-foundation", 1437 | "version": "v6.1.4", 1438 | "source": { 1439 | "type": "git", 1440 | "url": "https://github.com/symfony/http-foundation.git", 1441 | "reference": "18e0f106a32887bcebef757e5b39c88e39a08f20" 1442 | }, 1443 | "dist": { 1444 | "type": "zip", 1445 | "url": "https://api.github.com/repos/symfony/http-foundation/zipball/18e0f106a32887bcebef757e5b39c88e39a08f20", 1446 | "reference": "18e0f106a32887bcebef757e5b39c88e39a08f20", 1447 | "shasum": "" 1448 | }, 1449 | "require": { 1450 | "php": ">=8.1", 1451 | "symfony/deprecation-contracts": "^2.1|^3", 1452 | "symfony/polyfill-mbstring": "~1.1" 1453 | }, 1454 | "require-dev": { 1455 | "predis/predis": "~1.0", 1456 | "symfony/cache": "^5.4|^6.0", 1457 | "symfony/dependency-injection": "^5.4|^6.0", 1458 | "symfony/expression-language": "^5.4|^6.0", 1459 | "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", 1460 | "symfony/mime": "^5.4|^6.0", 1461 | "symfony/rate-limiter": "^5.2|^6.0" 1462 | }, 1463 | "suggest": { 1464 | "symfony/mime": "To use the file extension guesser" 1465 | }, 1466 | "type": "library", 1467 | "autoload": { 1468 | "psr-4": { 1469 | "Symfony\\Component\\HttpFoundation\\": "" 1470 | }, 1471 | "exclude-from-classmap": [ 1472 | "/Tests/" 1473 | ] 1474 | }, 1475 | "notification-url": "https://packagist.org/downloads/", 1476 | "license": [ 1477 | "MIT" 1478 | ], 1479 | "authors": [ 1480 | { 1481 | "name": "Fabien Potencier", 1482 | "email": "fabien@symfony.com" 1483 | }, 1484 | { 1485 | "name": "Symfony Community", 1486 | "homepage": "https://symfony.com/contributors" 1487 | } 1488 | ], 1489 | "description": "Defines an object-oriented layer for the HTTP specification", 1490 | "homepage": "https://symfony.com", 1491 | "support": { 1492 | "source": "https://github.com/symfony/http-foundation/tree/v6.1.4" 1493 | }, 1494 | "funding": [ 1495 | { 1496 | "url": "https://symfony.com/sponsor", 1497 | "type": "custom" 1498 | }, 1499 | { 1500 | "url": "https://github.com/fabpot", 1501 | "type": "github" 1502 | }, 1503 | { 1504 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1505 | "type": "tidelift" 1506 | } 1507 | ], 1508 | "time": "2022-08-19T14:27:04+00:00" 1509 | }, 1510 | { 1511 | "name": "symfony/polyfill-ctype", 1512 | "version": "v1.26.0", 1513 | "source": { 1514 | "type": "git", 1515 | "url": "https://github.com/symfony/polyfill-ctype.git", 1516 | "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" 1517 | }, 1518 | "dist": { 1519 | "type": "zip", 1520 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", 1521 | "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", 1522 | "shasum": "" 1523 | }, 1524 | "require": { 1525 | "php": ">=7.1" 1526 | }, 1527 | "provide": { 1528 | "ext-ctype": "*" 1529 | }, 1530 | "suggest": { 1531 | "ext-ctype": "For best performance" 1532 | }, 1533 | "type": "library", 1534 | "extra": { 1535 | "branch-alias": { 1536 | "dev-main": "1.26-dev" 1537 | }, 1538 | "thanks": { 1539 | "name": "symfony/polyfill", 1540 | "url": "https://github.com/symfony/polyfill" 1541 | } 1542 | }, 1543 | "autoload": { 1544 | "files": [ 1545 | "bootstrap.php" 1546 | ], 1547 | "psr-4": { 1548 | "Symfony\\Polyfill\\Ctype\\": "" 1549 | } 1550 | }, 1551 | "notification-url": "https://packagist.org/downloads/", 1552 | "license": [ 1553 | "MIT" 1554 | ], 1555 | "authors": [ 1556 | { 1557 | "name": "Gert de Pagter", 1558 | "email": "BackEndTea@gmail.com" 1559 | }, 1560 | { 1561 | "name": "Symfony Community", 1562 | "homepage": "https://symfony.com/contributors" 1563 | } 1564 | ], 1565 | "description": "Symfony polyfill for ctype functions", 1566 | "homepage": "https://symfony.com", 1567 | "keywords": [ 1568 | "compatibility", 1569 | "ctype", 1570 | "polyfill", 1571 | "portable" 1572 | ], 1573 | "support": { 1574 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" 1575 | }, 1576 | "funding": [ 1577 | { 1578 | "url": "https://symfony.com/sponsor", 1579 | "type": "custom" 1580 | }, 1581 | { 1582 | "url": "https://github.com/fabpot", 1583 | "type": "github" 1584 | }, 1585 | { 1586 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1587 | "type": "tidelift" 1588 | } 1589 | ], 1590 | "time": "2022-05-24T11:49:31+00:00" 1591 | }, 1592 | { 1593 | "name": "symfony/polyfill-intl-grapheme", 1594 | "version": "v1.26.0", 1595 | "source": { 1596 | "type": "git", 1597 | "url": "https://github.com/symfony/polyfill-intl-grapheme.git", 1598 | "reference": "433d05519ce6990bf3530fba6957499d327395c2" 1599 | }, 1600 | "dist": { 1601 | "type": "zip", 1602 | "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", 1603 | "reference": "433d05519ce6990bf3530fba6957499d327395c2", 1604 | "shasum": "" 1605 | }, 1606 | "require": { 1607 | "php": ">=7.1" 1608 | }, 1609 | "suggest": { 1610 | "ext-intl": "For best performance" 1611 | }, 1612 | "type": "library", 1613 | "extra": { 1614 | "branch-alias": { 1615 | "dev-main": "1.26-dev" 1616 | }, 1617 | "thanks": { 1618 | "name": "symfony/polyfill", 1619 | "url": "https://github.com/symfony/polyfill" 1620 | } 1621 | }, 1622 | "autoload": { 1623 | "files": [ 1624 | "bootstrap.php" 1625 | ], 1626 | "psr-4": { 1627 | "Symfony\\Polyfill\\Intl\\Grapheme\\": "" 1628 | } 1629 | }, 1630 | "notification-url": "https://packagist.org/downloads/", 1631 | "license": [ 1632 | "MIT" 1633 | ], 1634 | "authors": [ 1635 | { 1636 | "name": "Nicolas Grekas", 1637 | "email": "p@tchwork.com" 1638 | }, 1639 | { 1640 | "name": "Symfony Community", 1641 | "homepage": "https://symfony.com/contributors" 1642 | } 1643 | ], 1644 | "description": "Symfony polyfill for intl's grapheme_* functions", 1645 | "homepage": "https://symfony.com", 1646 | "keywords": [ 1647 | "compatibility", 1648 | "grapheme", 1649 | "intl", 1650 | "polyfill", 1651 | "portable", 1652 | "shim" 1653 | ], 1654 | "support": { 1655 | "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" 1656 | }, 1657 | "funding": [ 1658 | { 1659 | "url": "https://symfony.com/sponsor", 1660 | "type": "custom" 1661 | }, 1662 | { 1663 | "url": "https://github.com/fabpot", 1664 | "type": "github" 1665 | }, 1666 | { 1667 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1668 | "type": "tidelift" 1669 | } 1670 | ], 1671 | "time": "2022-05-24T11:49:31+00:00" 1672 | }, 1673 | { 1674 | "name": "symfony/polyfill-intl-normalizer", 1675 | "version": "v1.26.0", 1676 | "source": { 1677 | "type": "git", 1678 | "url": "https://github.com/symfony/polyfill-intl-normalizer.git", 1679 | "reference": "219aa369ceff116e673852dce47c3a41794c14bd" 1680 | }, 1681 | "dist": { 1682 | "type": "zip", 1683 | "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", 1684 | "reference": "219aa369ceff116e673852dce47c3a41794c14bd", 1685 | "shasum": "" 1686 | }, 1687 | "require": { 1688 | "php": ">=7.1" 1689 | }, 1690 | "suggest": { 1691 | "ext-intl": "For best performance" 1692 | }, 1693 | "type": "library", 1694 | "extra": { 1695 | "branch-alias": { 1696 | "dev-main": "1.26-dev" 1697 | }, 1698 | "thanks": { 1699 | "name": "symfony/polyfill", 1700 | "url": "https://github.com/symfony/polyfill" 1701 | } 1702 | }, 1703 | "autoload": { 1704 | "files": [ 1705 | "bootstrap.php" 1706 | ], 1707 | "psr-4": { 1708 | "Symfony\\Polyfill\\Intl\\Normalizer\\": "" 1709 | }, 1710 | "classmap": [ 1711 | "Resources/stubs" 1712 | ] 1713 | }, 1714 | "notification-url": "https://packagist.org/downloads/", 1715 | "license": [ 1716 | "MIT" 1717 | ], 1718 | "authors": [ 1719 | { 1720 | "name": "Nicolas Grekas", 1721 | "email": "p@tchwork.com" 1722 | }, 1723 | { 1724 | "name": "Symfony Community", 1725 | "homepage": "https://symfony.com/contributors" 1726 | } 1727 | ], 1728 | "description": "Symfony polyfill for intl's Normalizer class and related functions", 1729 | "homepage": "https://symfony.com", 1730 | "keywords": [ 1731 | "compatibility", 1732 | "intl", 1733 | "normalizer", 1734 | "polyfill", 1735 | "portable", 1736 | "shim" 1737 | ], 1738 | "support": { 1739 | "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" 1740 | }, 1741 | "funding": [ 1742 | { 1743 | "url": "https://symfony.com/sponsor", 1744 | "type": "custom" 1745 | }, 1746 | { 1747 | "url": "https://github.com/fabpot", 1748 | "type": "github" 1749 | }, 1750 | { 1751 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1752 | "type": "tidelift" 1753 | } 1754 | ], 1755 | "time": "2022-05-24T11:49:31+00:00" 1756 | }, 1757 | { 1758 | "name": "symfony/polyfill-mbstring", 1759 | "version": "v1.26.0", 1760 | "source": { 1761 | "type": "git", 1762 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1763 | "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" 1764 | }, 1765 | "dist": { 1766 | "type": "zip", 1767 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", 1768 | "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", 1769 | "shasum": "" 1770 | }, 1771 | "require": { 1772 | "php": ">=7.1" 1773 | }, 1774 | "provide": { 1775 | "ext-mbstring": "*" 1776 | }, 1777 | "suggest": { 1778 | "ext-mbstring": "For best performance" 1779 | }, 1780 | "type": "library", 1781 | "extra": { 1782 | "branch-alias": { 1783 | "dev-main": "1.26-dev" 1784 | }, 1785 | "thanks": { 1786 | "name": "symfony/polyfill", 1787 | "url": "https://github.com/symfony/polyfill" 1788 | } 1789 | }, 1790 | "autoload": { 1791 | "files": [ 1792 | "bootstrap.php" 1793 | ], 1794 | "psr-4": { 1795 | "Symfony\\Polyfill\\Mbstring\\": "" 1796 | } 1797 | }, 1798 | "notification-url": "https://packagist.org/downloads/", 1799 | "license": [ 1800 | "MIT" 1801 | ], 1802 | "authors": [ 1803 | { 1804 | "name": "Nicolas Grekas", 1805 | "email": "p@tchwork.com" 1806 | }, 1807 | { 1808 | "name": "Symfony Community", 1809 | "homepage": "https://symfony.com/contributors" 1810 | } 1811 | ], 1812 | "description": "Symfony polyfill for the Mbstring extension", 1813 | "homepage": "https://symfony.com", 1814 | "keywords": [ 1815 | "compatibility", 1816 | "mbstring", 1817 | "polyfill", 1818 | "portable", 1819 | "shim" 1820 | ], 1821 | "support": { 1822 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" 1823 | }, 1824 | "funding": [ 1825 | { 1826 | "url": "https://symfony.com/sponsor", 1827 | "type": "custom" 1828 | }, 1829 | { 1830 | "url": "https://github.com/fabpot", 1831 | "type": "github" 1832 | }, 1833 | { 1834 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1835 | "type": "tidelift" 1836 | } 1837 | ], 1838 | "time": "2022-05-24T11:49:31+00:00" 1839 | }, 1840 | { 1841 | "name": "symfony/polyfill-php72", 1842 | "version": "v1.26.0", 1843 | "source": { 1844 | "type": "git", 1845 | "url": "https://github.com/symfony/polyfill-php72.git", 1846 | "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" 1847 | }, 1848 | "dist": { 1849 | "type": "zip", 1850 | "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", 1851 | "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", 1852 | "shasum": "" 1853 | }, 1854 | "require": { 1855 | "php": ">=7.1" 1856 | }, 1857 | "type": "library", 1858 | "extra": { 1859 | "branch-alias": { 1860 | "dev-main": "1.26-dev" 1861 | }, 1862 | "thanks": { 1863 | "name": "symfony/polyfill", 1864 | "url": "https://github.com/symfony/polyfill" 1865 | } 1866 | }, 1867 | "autoload": { 1868 | "files": [ 1869 | "bootstrap.php" 1870 | ], 1871 | "psr-4": { 1872 | "Symfony\\Polyfill\\Php72\\": "" 1873 | } 1874 | }, 1875 | "notification-url": "https://packagist.org/downloads/", 1876 | "license": [ 1877 | "MIT" 1878 | ], 1879 | "authors": [ 1880 | { 1881 | "name": "Nicolas Grekas", 1882 | "email": "p@tchwork.com" 1883 | }, 1884 | { 1885 | "name": "Symfony Community", 1886 | "homepage": "https://symfony.com/contributors" 1887 | } 1888 | ], 1889 | "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", 1890 | "homepage": "https://symfony.com", 1891 | "keywords": [ 1892 | "compatibility", 1893 | "polyfill", 1894 | "portable", 1895 | "shim" 1896 | ], 1897 | "support": { 1898 | "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" 1899 | }, 1900 | "funding": [ 1901 | { 1902 | "url": "https://symfony.com/sponsor", 1903 | "type": "custom" 1904 | }, 1905 | { 1906 | "url": "https://github.com/fabpot", 1907 | "type": "github" 1908 | }, 1909 | { 1910 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1911 | "type": "tidelift" 1912 | } 1913 | ], 1914 | "time": "2022-05-24T11:49:31+00:00" 1915 | }, 1916 | { 1917 | "name": "symfony/polyfill-php80", 1918 | "version": "v1.26.0", 1919 | "source": { 1920 | "type": "git", 1921 | "url": "https://github.com/symfony/polyfill-php80.git", 1922 | "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" 1923 | }, 1924 | "dist": { 1925 | "type": "zip", 1926 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", 1927 | "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", 1928 | "shasum": "" 1929 | }, 1930 | "require": { 1931 | "php": ">=7.1" 1932 | }, 1933 | "type": "library", 1934 | "extra": { 1935 | "branch-alias": { 1936 | "dev-main": "1.26-dev" 1937 | }, 1938 | "thanks": { 1939 | "name": "symfony/polyfill", 1940 | "url": "https://github.com/symfony/polyfill" 1941 | } 1942 | }, 1943 | "autoload": { 1944 | "files": [ 1945 | "bootstrap.php" 1946 | ], 1947 | "psr-4": { 1948 | "Symfony\\Polyfill\\Php80\\": "" 1949 | }, 1950 | "classmap": [ 1951 | "Resources/stubs" 1952 | ] 1953 | }, 1954 | "notification-url": "https://packagist.org/downloads/", 1955 | "license": [ 1956 | "MIT" 1957 | ], 1958 | "authors": [ 1959 | { 1960 | "name": "Ion Bazan", 1961 | "email": "ion.bazan@gmail.com" 1962 | }, 1963 | { 1964 | "name": "Nicolas Grekas", 1965 | "email": "p@tchwork.com" 1966 | }, 1967 | { 1968 | "name": "Symfony Community", 1969 | "homepage": "https://symfony.com/contributors" 1970 | } 1971 | ], 1972 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 1973 | "homepage": "https://symfony.com", 1974 | "keywords": [ 1975 | "compatibility", 1976 | "polyfill", 1977 | "portable", 1978 | "shim" 1979 | ], 1980 | "support": { 1981 | "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" 1982 | }, 1983 | "funding": [ 1984 | { 1985 | "url": "https://symfony.com/sponsor", 1986 | "type": "custom" 1987 | }, 1988 | { 1989 | "url": "https://github.com/fabpot", 1990 | "type": "github" 1991 | }, 1992 | { 1993 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1994 | "type": "tidelift" 1995 | } 1996 | ], 1997 | "time": "2022-05-10T07:21:04+00:00" 1998 | }, 1999 | { 2000 | "name": "symfony/service-contracts", 2001 | "version": "v3.1.1", 2002 | "source": { 2003 | "type": "git", 2004 | "url": "https://github.com/symfony/service-contracts.git", 2005 | "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239" 2006 | }, 2007 | "dist": { 2008 | "type": "zip", 2009 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239", 2010 | "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239", 2011 | "shasum": "" 2012 | }, 2013 | "require": { 2014 | "php": ">=8.1", 2015 | "psr/container": "^2.0" 2016 | }, 2017 | "conflict": { 2018 | "ext-psr": "<1.1|>=2" 2019 | }, 2020 | "suggest": { 2021 | "symfony/service-implementation": "" 2022 | }, 2023 | "type": "library", 2024 | "extra": { 2025 | "branch-alias": { 2026 | "dev-main": "3.1-dev" 2027 | }, 2028 | "thanks": { 2029 | "name": "symfony/contracts", 2030 | "url": "https://github.com/symfony/contracts" 2031 | } 2032 | }, 2033 | "autoload": { 2034 | "psr-4": { 2035 | "Symfony\\Contracts\\Service\\": "" 2036 | }, 2037 | "exclude-from-classmap": [ 2038 | "/Test/" 2039 | ] 2040 | }, 2041 | "notification-url": "https://packagist.org/downloads/", 2042 | "license": [ 2043 | "MIT" 2044 | ], 2045 | "authors": [ 2046 | { 2047 | "name": "Nicolas Grekas", 2048 | "email": "p@tchwork.com" 2049 | }, 2050 | { 2051 | "name": "Symfony Community", 2052 | "homepage": "https://symfony.com/contributors" 2053 | } 2054 | ], 2055 | "description": "Generic abstractions related to writing services", 2056 | "homepage": "https://symfony.com", 2057 | "keywords": [ 2058 | "abstractions", 2059 | "contracts", 2060 | "decoupling", 2061 | "interfaces", 2062 | "interoperability", 2063 | "standards" 2064 | ], 2065 | "support": { 2066 | "source": "https://github.com/symfony/service-contracts/tree/v3.1.1" 2067 | }, 2068 | "funding": [ 2069 | { 2070 | "url": "https://symfony.com/sponsor", 2071 | "type": "custom" 2072 | }, 2073 | { 2074 | "url": "https://github.com/fabpot", 2075 | "type": "github" 2076 | }, 2077 | { 2078 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2079 | "type": "tidelift" 2080 | } 2081 | ], 2082 | "time": "2022-05-30T19:18:58+00:00" 2083 | }, 2084 | { 2085 | "name": "symfony/string", 2086 | "version": "v6.1.4", 2087 | "source": { 2088 | "type": "git", 2089 | "url": "https://github.com/symfony/string.git", 2090 | "reference": "290972cad7b364e3befaa74ba0ec729800fb161c" 2091 | }, 2092 | "dist": { 2093 | "type": "zip", 2094 | "url": "https://api.github.com/repos/symfony/string/zipball/290972cad7b364e3befaa74ba0ec729800fb161c", 2095 | "reference": "290972cad7b364e3befaa74ba0ec729800fb161c", 2096 | "shasum": "" 2097 | }, 2098 | "require": { 2099 | "php": ">=8.1", 2100 | "symfony/polyfill-ctype": "~1.8", 2101 | "symfony/polyfill-intl-grapheme": "~1.0", 2102 | "symfony/polyfill-intl-normalizer": "~1.0", 2103 | "symfony/polyfill-mbstring": "~1.0" 2104 | }, 2105 | "conflict": { 2106 | "symfony/translation-contracts": "<2.0" 2107 | }, 2108 | "require-dev": { 2109 | "symfony/error-handler": "^5.4|^6.0", 2110 | "symfony/http-client": "^5.4|^6.0", 2111 | "symfony/translation-contracts": "^2.0|^3.0", 2112 | "symfony/var-exporter": "^5.4|^6.0" 2113 | }, 2114 | "type": "library", 2115 | "autoload": { 2116 | "files": [ 2117 | "Resources/functions.php" 2118 | ], 2119 | "psr-4": { 2120 | "Symfony\\Component\\String\\": "" 2121 | }, 2122 | "exclude-from-classmap": [ 2123 | "/Tests/" 2124 | ] 2125 | }, 2126 | "notification-url": "https://packagist.org/downloads/", 2127 | "license": [ 2128 | "MIT" 2129 | ], 2130 | "authors": [ 2131 | { 2132 | "name": "Nicolas Grekas", 2133 | "email": "p@tchwork.com" 2134 | }, 2135 | { 2136 | "name": "Symfony Community", 2137 | "homepage": "https://symfony.com/contributors" 2138 | } 2139 | ], 2140 | "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", 2141 | "homepage": "https://symfony.com", 2142 | "keywords": [ 2143 | "grapheme", 2144 | "i18n", 2145 | "string", 2146 | "unicode", 2147 | "utf-8", 2148 | "utf8" 2149 | ], 2150 | "support": { 2151 | "source": "https://github.com/symfony/string/tree/v6.1.4" 2152 | }, 2153 | "funding": [ 2154 | { 2155 | "url": "https://symfony.com/sponsor", 2156 | "type": "custom" 2157 | }, 2158 | { 2159 | "url": "https://github.com/fabpot", 2160 | "type": "github" 2161 | }, 2162 | { 2163 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2164 | "type": "tidelift" 2165 | } 2166 | ], 2167 | "time": "2022-08-12T18:05:43+00:00" 2168 | }, 2169 | { 2170 | "name": "symfony/var-exporter", 2171 | "version": "v6.1.3", 2172 | "source": { 2173 | "type": "git", 2174 | "url": "https://github.com/symfony/var-exporter.git", 2175 | "reference": "b49350f45cebbba6e5286485264b912f2bcfc9ef" 2176 | }, 2177 | "dist": { 2178 | "type": "zip", 2179 | "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b49350f45cebbba6e5286485264b912f2bcfc9ef", 2180 | "reference": "b49350f45cebbba6e5286485264b912f2bcfc9ef", 2181 | "shasum": "" 2182 | }, 2183 | "require": { 2184 | "php": ">=8.1" 2185 | }, 2186 | "require-dev": { 2187 | "symfony/var-dumper": "^5.4|^6.0" 2188 | }, 2189 | "type": "library", 2190 | "autoload": { 2191 | "psr-4": { 2192 | "Symfony\\Component\\VarExporter\\": "" 2193 | }, 2194 | "exclude-from-classmap": [ 2195 | "/Tests/" 2196 | ] 2197 | }, 2198 | "notification-url": "https://packagist.org/downloads/", 2199 | "license": [ 2200 | "MIT" 2201 | ], 2202 | "authors": [ 2203 | { 2204 | "name": "Nicolas Grekas", 2205 | "email": "p@tchwork.com" 2206 | }, 2207 | { 2208 | "name": "Symfony Community", 2209 | "homepage": "https://symfony.com/contributors" 2210 | } 2211 | ], 2212 | "description": "Allows exporting any serializable PHP data structure to plain PHP code", 2213 | "homepage": "https://symfony.com", 2214 | "keywords": [ 2215 | "clone", 2216 | "construct", 2217 | "export", 2218 | "hydrate", 2219 | "instantiate", 2220 | "serialize" 2221 | ], 2222 | "support": { 2223 | "source": "https://github.com/symfony/var-exporter/tree/v6.1.3" 2224 | }, 2225 | "funding": [ 2226 | { 2227 | "url": "https://symfony.com/sponsor", 2228 | "type": "custom" 2229 | }, 2230 | { 2231 | "url": "https://github.com/fabpot", 2232 | "type": "github" 2233 | }, 2234 | { 2235 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2236 | "type": "tidelift" 2237 | } 2238 | ], 2239 | "time": "2022-07-04T16:01:56+00:00" 2240 | } 2241 | ], 2242 | "packages-dev": [], 2243 | "aliases": [], 2244 | "minimum-stability": "stable", 2245 | "stability-flags": [], 2246 | "prefer-stable": false, 2247 | "prefer-lowest": false, 2248 | "platform": { 2249 | "php": "^8.1" 2250 | }, 2251 | "platform-dev": [], 2252 | "plugin-api-version": "2.3.0" 2253 | } 2254 | -------------------------------------------------------------------------------- /config/doctrine/Article.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/router.php: -------------------------------------------------------------------------------- 1 | 0) ? floor(log($bytes, $mod)) : 0; 34 | } 35 | 36 | return sprintf($format, $bytes / pow($mod, $power), $units[$power]); 37 | } 38 | 39 | switch ($_SERVER['REQUEST_URI']) { 40 | case '/': 41 | $response = new Response(); 42 | $response->setStatusCode(200); 43 | $response->setContent(<< 45 | Articles 46 |

Articles

Loading ....
47 | 48 | EOT); 49 | $response->headers->set('Content-Type', 'text/html'); 50 | 51 | $response->send(); 52 | 53 | break; 54 | case '/articles.json': 55 | $entityManager = EntityManagerFactory::getEntityManagerFactory(); 56 | $action = new ArticleListAction(); 57 | $response = $action($entityManager); 58 | $response->send(); 59 | 60 | $memoryUsage = memory_get_usage(true); 61 | $memoryPeakUsage = memory_get_peak_usage(true); 62 | 63 | file_put_contents( 64 | __DIR__ . '/../var/memory-usage.txt', 65 | bytes($memoryUsage) . PHP_EOL . bytes($memoryPeakUsage) 66 | ); 67 | break; 68 | case '/symfony-articles.json': 69 | $entityManager = EntityManagerFactory::getEntityManagerFactory(); 70 | $action = new ArticleListSymfonyAction(); 71 | $response = $action($entityManager); 72 | $response->send(); 73 | 74 | $memoryUsage = memory_get_usage(true); 75 | $memoryPeakUsage = memory_get_peak_usage(true); 76 | 77 | file_put_contents( 78 | __DIR__ . '/../var/memory-usage-symfony.txt', 79 | bytes($memoryUsage) . PHP_EOL . bytes($memoryPeakUsage) 80 | ); 81 | 82 | break; 83 | case '/old-articles.json': 84 | $entityManager = EntityManagerFactory::getEntityManagerFactory(); 85 | $action = new ArticleListOldAction(); 86 | $response = $action($entityManager); 87 | $response->send(); 88 | 89 | $memoryUsage = memory_get_usage(true); 90 | $memoryPeakUsage = memory_get_peak_usage(true); 91 | 92 | file_put_contents( 93 | __DIR__ . '/../var/memory-usage-old.txt', 94 | bytes($memoryUsage) . PHP_EOL . bytes($memoryPeakUsage) 95 | ); 96 | 97 | break; 98 | case '/old-iterable-articles.json': 99 | $entityManager = EntityManagerFactory::getEntityManagerFactory(); 100 | $action = new ArticleListOldIterableAction(); 101 | $response = $action($entityManager); 102 | $response->send(); 103 | 104 | $memoryUsage = memory_get_usage(true); 105 | $memoryPeakUsage = memory_get_peak_usage(true); 106 | 107 | file_put_contents( 108 | __DIR__ . '/../var/memory-usage-old-iterable.txt', 109 | bytes($memoryUsage) . PHP_EOL . bytes($memoryPeakUsage) 110 | ); 111 | 112 | break; 113 | default: 114 | $response = new Response(); 115 | $response->setStatusCode(404); 116 | $response->setContent('Error 404 - Page not found.'); 117 | 118 | $response->send(); 119 | 120 | break; 121 | } 122 | -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | async function* loadLineByLine(url) { 2 | const utf8Decoder = new TextDecoder('utf-8'); 3 | const response = await fetch(url); 4 | const reader = response.body.getReader(); 5 | let { value: chunk, done: readerDone } = await reader.read(); 6 | chunk = chunk ? utf8Decoder.decode(chunk) : ''; 7 | 8 | const re = /\n|\r|\r\n/gm; 9 | let startIndex = 0; 10 | let result; 11 | 12 | for (;;) { 13 | let result = re.exec(chunk); 14 | if (!result) { 15 | if (readerDone) { 16 | break; 17 | } 18 | let remainder = chunk.substr(startIndex); 19 | ({ value: chunk, done: readerDone } = await reader.read()); 20 | chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ''); 21 | startIndex = re.lastIndex = 0; 22 | continue; 23 | } 24 | 25 | yield chunk.substring(startIndex, result.index); 26 | startIndex = re.lastIndex; 27 | } 28 | 29 | if (startIndex < chunk.length) { 30 | // last line didn't end in a newline char 31 | yield chunk.substr(startIndex); 32 | } 33 | } 34 | 35 | async function run(itemCallback, contentCallback) { 36 | let content = ''; 37 | 38 | for await (let line of loadLineByLine('articles.json')) { 39 | try { 40 | const object = JSON.parse(line.replace(/\,$/, '')); 41 | 42 | itemCallback(object); 43 | } catch (e) { 44 | content += line; 45 | 46 | continue; 47 | } 48 | } 49 | 50 | contentCallback(content); 51 | } 52 | 53 | const list = document.getElementById('list'); 54 | const loader = document.getElementById('loading'); 55 | let counter = 0; 56 | 57 | run((object) => { 58 | const tr = document.createElement('tr'); 59 | tr.innerHTML = 60 | '' + object.id + '' 61 | + '' + object.title + '' 62 | + '' + object.description + ''; 63 | 64 | list.append(tr); 65 | 66 | ++counter; 67 | loader.innerText = 'Loaded ' + counter; 68 | }, (content) => { 69 | loader.innerText = 'Loaded: ' + content; 70 | }); 71 | -------------------------------------------------------------------------------- /src/Controller/ArticleListAction.php: -------------------------------------------------------------------------------- 1 | findArticles($entityManager); 15 | 16 | return new StreamedResponse(function() use ($articles) { 17 | // defining our json structure but replaces the articles with a placeholder 18 | $jsonStructure = json_encode([ 19 | 'embedded' => [ 20 | 'articles' => ['__REPLACES_ARTICLES__'], 21 | ], 22 | 'total' => 100_000, 23 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 24 | 25 | // split by placeholder 26 | [$before, $after] = explode('"__REPLACES_ARTICLES__"', $jsonStructure, 2); 27 | 28 | // send first before part of the json 29 | echo $before . PHP_EOL; 30 | 31 | // stream article one by one as own json 32 | foreach ($articles as $count => $article) { 33 | if ($count !== 0) { 34 | echo ',' . PHP_EOL; // if not first element we need a separator 35 | } 36 | 37 | if ($count % 500 === 0 && $count !== 100_000) { // flush response after every 500 38 | flush(); 39 | } 40 | 41 | echo json_encode($article, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 42 | } 43 | 44 | // send the after part of the json as last 45 | echo PHP_EOL; 46 | echo $after; 47 | 48 | }, 200, ['Content-Type' => 'application/json']); 49 | } 50 | 51 | private function findArticles(EntityManagerInterface $entityManager): iterable 52 | { 53 | $queryBuilder = $entityManager->createQueryBuilder(); 54 | $queryBuilder->from(Article::class, 'article'); 55 | $queryBuilder->select('article.id') 56 | ->addSelect('article.title') 57 | ->addSelect('article.description'); 58 | 59 | return $queryBuilder->getQuery()->toIterable(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Controller/ArticleListOldAction.php: -------------------------------------------------------------------------------- 1 | findArticles($entityManager); 16 | 17 | return JsonResponse::fromJsonString(json_encode([ 18 | 'embedded' => [ 19 | 'articles' => $articles, 20 | 'total' => 100_000, 21 | ], 22 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 23 | } 24 | 25 | private function findArticles(EntityManagerInterface $entityManager): iterable 26 | { 27 | $queryBuilder = $entityManager->createQueryBuilder(); 28 | $queryBuilder->from(Article::class, 'article'); 29 | $queryBuilder->select('article.id') 30 | ->addSelect('article.title') 31 | ->addSelect('article.description'); 32 | 33 | return $queryBuilder->getQuery()->getResult(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Controller/ArticleListOldIterableAction.php: -------------------------------------------------------------------------------- 1 | findArticles($entityManager)); 16 | 17 | return JsonResponse::fromJsonString(json_encode([ 18 | 'embedded' => [ 19 | 'articles' => $articles, 20 | 'total' => 100_000, 21 | ], 22 | ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 23 | } 24 | 25 | private function findArticles(EntityManagerInterface $entityManager): iterable 26 | { 27 | $queryBuilder = $entityManager->createQueryBuilder(); 28 | $queryBuilder->from(Article::class, 'article'); 29 | $queryBuilder->select('article.id') 30 | ->addSelect('article.title') 31 | ->addSelect('article.description'); 32 | 33 | return $queryBuilder->getQuery()->toIterable(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Controller/ArticleListSymfonyAction.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'articles' => $this->findArticles($entityManager), 16 | ], 17 | 'total' => 100_000, 18 | ]); 19 | } 20 | 21 | private function findArticles(EntityManagerInterface $entityManager): \Generator 22 | { 23 | $queryBuilder = $entityManager->createQueryBuilder(); 24 | $queryBuilder->from(Article::class, 'article'); 25 | $queryBuilder->select('article.id') 26 | ->addSelect('article.title') 27 | ->addSelect('article.description'); 28 | 29 | $count = 0; 30 | foreach ($queryBuilder->getQuery()->toIterable() as $key => $value) { 31 | yield $key => $value; 32 | 33 | ++$count; 34 | if ($count % 500 === 0 && $count !== 100_000) { // flush response after every 500 35 | flush(); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/StreamedJsonResponse.php: -------------------------------------------------------------------------------- 1 | 19 | * 20 | * Example usage: 21 | * 22 | * function loadArticles(): \Generator 23 | * // some streamed loading 24 | * yield ['title' => 'Article 1']; 25 | * yield ['title' => 'Article 2']; 26 | * yield ['title' => 'Article 3']; 27 | * // recommended to use flush() after every specific number of items 28 | * }), 29 | * 30 | * $response = new StreamedJsonResponse( 31 | * // json structure with generators in which will be streamed 32 | * [ 33 | * '_embedded' => [ 34 | * 'articles' => loadArticles(), // any generator which you want to stream as list of data 35 | * ], 36 | * ], 37 | * ); 38 | */ 39 | class StreamedJsonResponse extends StreamedResponse 40 | { 41 | private const PLACEHOLDER = '__symfony_json__'; 42 | 43 | /** 44 | * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data 45 | * @param int $status The HTTP status code (200 "OK" by default) 46 | * @param array $headers An array of HTTP headers 47 | * @param int $encodingOptions Flags for the json_encode() function 48 | */ 49 | public function __construct( 50 | private readonly array $data, 51 | int $status = 200, 52 | array $headers = [], 53 | private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, 54 | ) { 55 | parent::__construct($this->stream(...), $status, $headers); 56 | 57 | if (!$this->headers->get('Content-Type')) { 58 | $this->headers->set('Content-Type', 'application/json'); 59 | } 60 | } 61 | 62 | private function stream(): void 63 | { 64 | $generators = []; 65 | $structure = $this->data; 66 | 67 | array_walk_recursive($structure, function (&$item, $key) use (&$generators) { 68 | if (self::PLACEHOLDER === $key) { 69 | // if the placeholder is already in the structure it should be replaced with a new one that explode 70 | // works like expected for the structure 71 | $generators[] = $item; 72 | } 73 | 74 | // generators should be used but for better DX all kind of Traversable are supported 75 | if ($item instanceof \Traversable || $item instanceof \JsonSerializable) { 76 | $generators[] = $item; 77 | $item = self::PLACEHOLDER; 78 | } elseif (self::PLACEHOLDER === $item) { 79 | // if the placeholder is already in the structure it should be replaced with a new one that explode 80 | // works like expected for the structure 81 | $generators[] = $item; 82 | } 83 | }); 84 | 85 | $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; 86 | $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; 87 | 88 | $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($structure, $jsonEncodingOptions)); 89 | 90 | foreach ($generators as $index => $generator) { 91 | // send first and between parts of the structure 92 | echo $jsonParts[$index]; 93 | 94 | if (self::PLACEHOLDER === $generator || $generator instanceof \JsonSerializable) { 95 | // the placeholders already in the structure are rendered here 96 | echo json_encode($generator, $jsonEncodingOptions); 97 | 98 | continue; 99 | } 100 | 101 | $isFirstItem = true; 102 | $startTag = '['; 103 | foreach ($generator as $key => $item) { 104 | if ($isFirstItem) { 105 | $isFirstItem = false; 106 | // depending on the first elements key the generator is detected as a list or map 107 | // we can not check for a whole list or map because that would hurt the performance 108 | // of the streamed response which is the main goal of this response class 109 | if (0 !== $key) { 110 | $startTag = '{'; 111 | } 112 | 113 | echo $startTag; 114 | } else { 115 | // if not first element of the generic, a separator is required between the elements 116 | echo ','; 117 | } 118 | 119 | if ('{' === $startTag) { 120 | echo json_encode((string) $key, $keyEncodingOptions).':'; 121 | } 122 | 123 | echo json_encode($item, $jsonEncodingOptions); 124 | } 125 | 126 | echo '[' === $startTag ? ']' : '}'; 127 | } 128 | 129 | // send last part of the structure 130 | echo $jsonParts[array_key_last($jsonParts)]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Doctrine/EntityManagerFactory.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 34 | 'path' => static::$DATABASE_FILE, 35 | ]; 36 | 37 | $cache = new FilesystemAdapter('doctrine', 0, __DIR__ . '/../../var/cache'); 38 | 39 | $config = ORMSetup::createConfiguration( 40 | false, 41 | __DIR__ . '/../../var/cache', 42 | $cache 43 | ); 44 | 45 | $namespaces = [ 46 | __DIR__ . '/../../config/doctrine' => 'App\Entity', 47 | ]; 48 | 49 | $driver = new SimplifiedXmlDriver($namespaces); 50 | 51 | $config->setMetadataDriverImpl($driver); 52 | 53 | $eventManager = new EventManager(); 54 | $eventManager->addEventListener(Events::loadClassMetadata, new ResolveTargetEntityListener()); 55 | static::$entityManager = EntityManager::create($connection, $config, $eventManager); 56 | 57 | if (!file_exists(static::$DATABASE_FILE)) { 58 | $schemaTool = new SchemaTool(static::$entityManager); 59 | $classes = static::$entityManager->getMetadataFactory()->getAllMetadata(); 60 | $schemaTool->createSchema($classes); 61 | 62 | // load fixtures 63 | for ($i = 0; $i <= 100_000; ++$i) { 64 | $article = new Article('Title ' . $i, 'Description ' . $i . PHP_EOL . 'More description text ....'); 65 | static::$entityManager->persist($article); 66 | 67 | if ($i % 100 === 0) { 68 | static::$entityManager->flush(); 69 | static::$entityManager->clear(); 70 | } 71 | } 72 | 73 | static::$entityManager->flush(); 74 | static::$entityManager->clear(); 75 | } 76 | 77 | return static::$entityManager; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Entity/Article.php: -------------------------------------------------------------------------------- 1 | title = $title; 16 | $this->description = $description; 17 | } 18 | 19 | public function getId(): string 20 | { 21 | return $this->title; 22 | } 23 | 24 | public function getTitle(): string 25 | { 26 | return $this->title; 27 | } 28 | 29 | public function getDescription(): string 30 | { 31 | return $this->description; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /var/memory-usage-old-iterable.txt: -------------------------------------------------------------------------------- 1 | 62.11 MB 2 | 71.79 MB -------------------------------------------------------------------------------- /var/memory-usage-old.txt: -------------------------------------------------------------------------------- 1 | 64.21 MB 2 | 73.89 MB -------------------------------------------------------------------------------- /var/memory-usage-symfony.txt: -------------------------------------------------------------------------------- 1 | 2.10 MB 2 | 2.10 MB -------------------------------------------------------------------------------- /var/memory-usage.txt: -------------------------------------------------------------------------------- 1 | 2.10 MB 2 | 2.10 MB -------------------------------------------------------------------------------- /var/test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-schranz/efficient-json-streaming-with-symfony-doctrine/650abab20ce807872aa5a3c92617f80b18eebd68/var/test.sqlite --------------------------------------------------------------------------------