├── .github └── workflows │ └── app.yaml ├── .gitignore ├── LICENCE ├── Makefile ├── README.md ├── backend ├── .env ├── .env.test ├── .gitignore ├── .php-cs-fixer.php ├── bin │ ├── console │ └── phpunit ├── compose.override.yaml ├── compose.yaml ├── composer.json ├── composer.lock ├── config │ ├── bundles.php │ ├── packages │ │ ├── api_platform.yaml │ │ ├── cache.yaml │ │ ├── csrf.yaml │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── doctrine.yaml │ │ ├── doctrine_migrations.yaml │ │ ├── framework.yaml │ │ ├── mailer.yaml │ │ ├── nelmio_cors.yaml │ │ ├── routing.yaml │ │ ├── security.yaml │ │ ├── translation.yaml │ │ ├── twig.yaml │ │ ├── twig_component.yaml │ │ ├── validator.yaml │ │ └── web_profiler.yaml │ ├── preload.php │ ├── routes.yaml │ ├── routes │ │ ├── api_platform.yaml │ │ ├── framework.yaml │ │ ├── security.yaml │ │ └── web_profiler.yaml │ └── services.yaml ├── migrations │ ├── .gitignore │ └── Version20241021202805.php ├── phpunit.xml.dist ├── public │ └── index.php ├── src │ ├── ApiResource │ │ ├── .gitignore │ │ └── Book.php │ ├── Controller │ │ ├── .gitignore │ │ └── Admin │ │ │ ├── BookCrudController.php │ │ │ └── DashboardController.php │ ├── DataFixtures │ │ └── BookFixtures.php │ ├── Entity │ │ ├── .gitignore │ │ └── Book.php │ ├── Kernel.php │ └── Repository │ │ ├── .gitignore │ │ └── BookRepository.php ├── symfony.lock ├── templates │ └── base.html.twig ├── tests │ ├── Admin │ │ ├── AdminBooksTest.php │ │ └── AdminListTestUtils.php │ ├── AllRoutesTest.php │ ├── Api │ │ └── ApiGetBooksTest.php │ └── bootstrap.php └── translations │ └── .gitignore ├── compose.override.yaml ├── compose.yaml ├── docker ├── mailcatcher │ └── Dockerfile ├── node │ ├── Dockerfile │ └── bin │ │ └── entrypoint.sh └── php │ ├── Caddyfile │ ├── Dockerfile │ ├── bin │ └── entrypoint.sh │ └── etc │ └── php.ini ├── frontend ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── e2e │ └── demo.test.ts ├── eslint.config.js ├── orval.config.ts ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── src │ ├── app.d.ts │ ├── app.html │ ├── app.scss │ ├── demo.spec.ts │ ├── lib │ │ ├── components │ │ │ ├── TopMenu.svelte │ │ │ └── TopMenu.test.ts │ │ └── index.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ └── books │ │ ├── +page.svelte │ │ └── [id] │ │ ├── +page.svelte │ │ └── +page.ts ├── static │ ├── favicon.ico │ └── favicon.png ├── svelte.config.js ├── tsconfig.json └── vite.config.ts └── update_dependencies.bash /.github/workflows/app.yaml: -------------------------------------------------------------------------------- 1 | name: Application CI 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - run: docker compose pull 17 | 18 | - name: Docker Layer Caching 19 | uses: satackey/action-docker-layer-caching@v0.0.11 20 | continue-on-error: true 21 | 22 | - name: '[Run] Install the project' 23 | id: install 24 | run: | 25 | sudo make install 26 | 27 | - name: '[Run] 🚀 Test (backend)' 28 | run: make test-backend 29 | 30 | - name: '[Run] 🚀 Test (frontend)' 31 | run: make test-frontend 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker-compose.override.yaml 2 | frontend/src/openapi/ 3 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | Template for web applications made with PHP, Symfony, API Platform, EasyAdmin, Svelte Kit, Typescript and Docker. 474 | Copyright (C) 2022 Alex "Pierstoval" Rock 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Custom project config 2 | 3 | PHP_CONTAINER_NAME ?= php 4 | PHP_APP_DIR ?= backend 5 | NODE_CONTAINER_NAME ?= node 6 | NODE_APP_DIR ?= frontend 7 | NODE_PKG_MANAGER_NAME ?= pnpm 8 | 9 | # -------------- 10 | 11 | _WARN := "\033[33m[WARNING]\033[0m %s\n" # Yellow text for "printf" 12 | _INFO := "\033[32m[INFO]\033[0m %s\n" # Green text for "printf" 13 | _ERROR := "\033[31m[ERROR]\033[0m %s\n" # Red text for "printf" 14 | 15 | SHELL=bash 16 | 17 | DOCKER ?= docker 18 | DOCKER_COMPOSE ?= docker compose 19 | DOCKER_COMPOSE_EXEC ?= $(DOCKER_COMPOSE) exec 20 | DOCKER_COMPOSE_RUN ?= $(DOCKER_COMPOSE) run --rm 21 | 22 | PHP ?= $(DOCKER_COMPOSE_EXEC) --workdir=/app/backend $(PHP_CONTAINER_NAME) docker-php-entrypoint 23 | COMPOSER ?= $(DOCKER_COMPOSE_RUN) --workdir=/app/backend $(PHP_CONTAINER_NAME) composer 24 | SF_CONSOLE ?= $(PHP) bin/console 25 | NODE ?= $(DOCKER_COMPOSE_EXEC) -T $(NODE_CONTAINER_NAME) entrypoint 26 | NODE_PKG_MANAGER ?= $(NODE) $(NODE_PKG_MANAGER_NAME) 27 | NODE_PKG_MANAGER_RUN ?= $(DOCKER_COMPOSE_RUN) --no-deps $(NODE_CONTAINER_NAME) $(NODE_PKG_MANAGER_NAME) 28 | 29 | ## 30 | ## Project 31 | ## ======= 32 | ## 33 | 34 | .DEFAULT_GOAL := help 35 | help: ## Show this help message 36 | @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-25s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' 37 | .PHONY: help 38 | 39 | install: ## Install and start the project 40 | install: build node_modules start e2e-setup vendor db test-db openapi-export 41 | .PHONY: install 42 | 43 | build: ## Build the Docker images 44 | @$(DOCKER_COMPOSE) build --compress 45 | .PHONY: build 46 | 47 | start: ## Start all containers and the PHP server 48 | @$(DOCKER_COMPOSE) up -d --remove-orphans 49 | .PHONY: start 50 | 51 | stop: ## Stop all containers and the PHP server 52 | @$(DOCKER_COMPOSE) stop 53 | .PHONY: stop 54 | 55 | restart: stop start## Restart the containers & the PHP server 56 | .PHONY: restart 57 | 58 | kill: ## Stop all containers 59 | $(DOCKER_COMPOSE) kill 60 | $(DOCKER_COMPOSE) down --volumes --remove-orphans 61 | .PHONY: kill 62 | 63 | reset: ## Stop and start a fresh install of the project 64 | reset: kill install 65 | .PHONY: reset 66 | 67 | clean: ## Stop the project and remove generated files and configuration 68 | clean: kill 69 | git clean --force -d -x -- \ 70 | $(PHP_APP_DIR)/ \ 71 | $(NODE_APP_DIR)/ \ 72 | 73 | .PHONY: clean 74 | 75 | vendor: ## Install PHP vendors 76 | $(COMPOSER) install 77 | .PHONY: vendor 78 | 79 | node_modules: ## Install JS vendors 80 | mkdir -p $(NODE_APP_DIR)/node_modules/ 81 | $(NODE_PKG_MANAGER_RUN) install --frozen-lockfile --force 82 | $(DOCKER_COMPOSE) up -d $(NODE_CONTAINER_NAME) 83 | .PHONY: node_modules 84 | 85 | openapi-export: ## Export OpenAPI data to JSON and create a JS client for frontend use 86 | $(SF_CONSOLE) --no-interaction api:openapi:export --output=var/openapi/openapi.json 87 | $(NODE) mkdir -p build 88 | @$(PHP) chown -R 1000:1000 var # Bit hacky, isn't it... But it should work 89 | @$(NODE) chown -R 1000:1000 build # same hack here 90 | cp $(PHP_APP_DIR)/var/openapi/openapi.json \ 91 | $(NODE_APP_DIR)/build/openapi.json 92 | $(NODE_PKG_MANAGER_RUN) orval 93 | .PHONY: openapi-export 94 | 95 | ## 96 | ## Backend application 97 | ## ------------------- 98 | ## 99 | 100 | db: 101 | $(SF_CONSOLE) --no-interaction doctrine:database:drop --force --if-exists 102 | $(SF_CONSOLE) --no-interaction doctrine:database:create 103 | $(SF_CONSOLE) --no-interaction doctrine:migration:migrate --allow-no-migration 104 | $(SF_CONSOLE) --no-interaction doctrine:fixtures:load 105 | .PHONY: db 106 | 107 | test-db: 108 | $(SF_CONSOLE) --no-interaction --env=test doctrine:database:drop --force --if-exists 109 | $(SF_CONSOLE) --no-interaction --env=test doctrine:database:create 110 | $(SF_CONSOLE) --no-interaction --env=test doctrine:migration:migrate --allow-no-migration 111 | $(SF_CONSOLE) --no-interaction --env=test doctrine:fixtures:load 112 | .PHONY: test-db 113 | 114 | test-backend: ## Run backend tests 115 | $(PHP) bin/phpunit 116 | .PHONY: test-backend 117 | 118 | php-cs: ## Run php-cs-fixer to format PHP files 119 | $(PHP) php-cs-fixer fix 120 | .PHONY: php-cs 121 | 122 | ## 123 | ## Frontend application 124 | ## -------------------- 125 | ## 126 | 127 | assets-build: ## Build frontend as static site 128 | $(NODE_PKG_MANAGER) build 129 | .PHONY: assets-build 130 | 131 | e2e-setup: 132 | $(DOCKER_COMPOSE_EXEC) $(NODE_CONTAINER_NAME) $(NODE_PKG_MANAGER_NAME) run playwright install-deps 133 | $(NODE) $(NODE_PKG_MANAGER_NAME) run playwright install 134 | .PHONY: e2e-setup 135 | 136 | test-frontend: ## Run frontend tests 137 | $(NODE_PKG_MANAGER) run test 138 | .PHONY: test-frontend 139 | 140 | prettier: ## Run Prettier on JS/TS files to format them properly 141 | $(NODE_PKG_MANAGER) run format 142 | .PHONY: prettier 143 | 144 | ## 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Default JAMStack project 2 | ======================== 3 | 4 | Contains: 5 | 6 | * A `Makefile` that self-documents a lot of commands to run on the project 7 | * A whole Docker-based setup for development 8 | * PostgreSQL as default database 9 | * A standard static-site SvelteKit application for frontend 10 | * A PHP/Symfony app with Api Platform for backend 11 | * An OpenApi spec export/import process to allow frontend client to call the backend API 12 | * An administration panel powered by EasyAdmin 13 | * Unit tests made simple with Vitest 14 | * E2E/Browser testing with Playwright 15 | * Some default droppable code to allow you to quickly get into the project's whereabouts 16 | 17 | ## Create a new project 18 | 19 | There are two solutions: 20 | 21 | * Click on the Use this template button on the top-right of this repository's page, this will create a repository in whichever Github profile or organization you want. 22 | * Run one of these scripts: 23 | * With Git: 24 | ``` 25 | git clone https://github.com/Pierstoval/php-js-boilerplate.git && cd php-js-boilerplate && rm -rf .git/ && git init && git checkout -b main && git add . && git commit -a -m "Init project" 26 | ``` 27 | * With curl and unzip: 28 | ``` 29 | curl -sSL "https://github.com/Pierstoval/php-js-boilerplate/archive/refs/heads/main.zip" -o main.zip && unzip main.zip && rm main.zip && cd php-js-boilerplate-main && rm -rf .git/ && git init && git checkout -b main && git add . && git commit -a -m "Init project" 30 | ``` 31 | * With wget and unzip: 32 | ``` 33 | wget "https://github.com/Pierstoval/php-js-boilerplate/archive/refs/heads/main.zip" && unzip main.zip && rm main.zip && cd php-js-boilerplate-main && rm -rf .git/ && git init && git checkout -b main && git add . && git commit -a -m "Init project" 34 | ``` 35 | 36 | ## Usage 37 | 38 | Run `make install`. 39 | 40 | Then, you can visit the different parts of the app: 41 | 42 | * [https://localhost/](https://localhost/) for the frontend app. 43 | * [https://localhost/api/](https://localhost/api/) for the API. 44 | * [https://localhost/admin/](https://localhost/admin/) for the administration panel. 45 | 46 | ## HTTP 47 | 48 | The application is served through several endpoints: 49 | 50 | * Static file server for all the assets in `backend/public/` 51 | * PHP for the backend server, serving the API and administration panel. 52 | * Node.js for the SvelteKit frontend server. 53 | * A WebSocket connection, passed to the frontend server for HMR (Hot Module Replacement). This is a behavior that is not present in production mode. 54 | 55 | All of these different HTTP endpoints are configured using [Caddy](https://caddyserver.com), one of the most modern and customizable HTTP servers. 56 | 57 | Check also the [Caddyfile](./docker/php/Caddyfile) for details about HTTP routing. 58 | 59 | ## Backend 60 | 61 | The backend app exposes **only two** endpoints: 62 | 63 | * An API through the `/api` endpoint, and it is defined with Api Platform. 64 | * An administration panel via `/admin`, configured with EasyAdmin. 65 | 66 | You can customize the API and admin panel like you want, but if you need to change the **HTTP paths**, don't forget to check the [Caddyfile](./docker/php/Caddyfile) to update HTTP routing. 67 | 68 | ## Frontend 69 | 70 | The frontend is served via SvelteKit, and is only a fallback after all other HTTP routes (static files, websocket and API). 71 | 72 | So basically, everything that is not a file in the `backend/public/` directory and that does not begin with `/api` or `/admin` will be served by SvelteKit. 73 | 74 | ### API 75 | 76 | The API is defined in the Api Platform schema, and exported to OpenAPI. 77 | 78 | This OpenAPI export is then used by the frontend app, with the help of the [orval](https://orval.dev/) CLI tool, to generate an HTTP client with endpoints, structs/classes and [axios](https://axios-http.com) as HTTP client. 79 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # https://symfony.com/doc/current/configuration/secrets.html 13 | # 14 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 15 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 16 | 17 | ###> symfony/framework-bundle ### 18 | APP_ENV=dev 19 | APP_SECRET=d9dc2b33989c8fc759d81fc5a7f74d81 20 | ###< symfony/framework-bundle ### 21 | 22 | ###> doctrine/doctrine-bundle ### 23 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 24 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 25 | # 26 | # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" 27 | # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" 28 | # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" 29 | DATABASE_URL="postgresql://app:app@database:5432/app?serverVersion=16&charset=utf8" 30 | ###< doctrine/doctrine-bundle ### 31 | 32 | ###> nelmio/cors-bundle ### 33 | CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' 34 | ###< nelmio/cors-bundle ### 35 | 36 | ###> symfony/mailer ### 37 | MAILER_DSN=smtp://mailer:1025 38 | ###< symfony/mailer ### 39 | -------------------------------------------------------------------------------- /backend/.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> symfony/phpunit-bridge ### 13 | .phpunit.result.cache 14 | /phpunit.xml 15 | ###< symfony/phpunit-bridge ### 16 | 17 | ###> phpunit/phpunit ### 18 | /phpunit.xml 19 | .phpunit.result.cache 20 | ###< phpunit/phpunit ### 21 | -------------------------------------------------------------------------------- /backend/.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 7 | ->exclude('var') 8 | ; 9 | 10 | return (new PhpCsFixer\Config()) 11 | ->setRules([ 12 | '@Symfony' => true, 13 | ]) 14 | ->setFinder($finder) 15 | ; 16 | -------------------------------------------------------------------------------- /backend/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 80000) { 10 | require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; 11 | } else { 12 | define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); 13 | require PHPUNIT_COMPOSER_INSTALL; 14 | PHPUnit\TextUI\Command::main(); 15 | } 16 | } else { 17 | if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { 18 | echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; 19 | exit(1); 20 | } 21 | 22 | require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; 23 | } 24 | -------------------------------------------------------------------------------- /backend/compose.override.yaml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | ###> doctrine/doctrine-bundle ### 4 | database: 5 | ports: 6 | - "5432" 7 | ###< doctrine/doctrine-bundle ### 8 | 9 | ###> symfony/mailer ### 10 | mailer: 11 | image: axllent/mailpit 12 | ports: 13 | - "1025" 14 | - "8025" 15 | environment: 16 | MP_SMTP_AUTH_ACCEPT_ANY: 1 17 | MP_SMTP_AUTH_ALLOW_INSECURE: 1 18 | ###< symfony/mailer ### 19 | -------------------------------------------------------------------------------- /backend/compose.yaml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | ###> doctrine/doctrine-bundle ### 4 | database: 5 | image: postgres:${POSTGRES_VERSION:-16}-alpine 6 | environment: 7 | POSTGRES_DB: ${POSTGRES_DB:-app} 8 | # You should definitely change the password in production 9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-app} 10 | POSTGRES_USER: ${POSTGRES_USER:-app} 11 | healthcheck: 12 | test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"] 13 | timeout: 5s 14 | retries: 5 15 | start_period: 60s 16 | volumes: 17 | - database_data:/var/lib/postgresql/data:rw 18 | # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! 19 | # - ./docker/db/data:/var/lib/postgresql/data:rw 20 | ###< doctrine/doctrine-bundle ### 21 | 22 | volumes: 23 | ###> doctrine/doctrine-bundle ### 24 | database_data: 25 | ###< doctrine/doctrine-bundle ### 26 | -------------------------------------------------------------------------------- /backend/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "minimum-stability": "stable", 5 | "prefer-stable": true, 6 | "require": { 7 | "php": ">=8.2", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "api-platform/doctrine-orm": "^4.1.12", 11 | "api-platform/symfony": "^4.1.12", 12 | "doctrine/dbal": "^4.2.3", 13 | "doctrine/doctrine-bundle": "^2.14", 14 | "doctrine/doctrine-migrations-bundle": "^3.4.2", 15 | "doctrine/orm": "^3.3.3", 16 | "easycorp/easyadmin-bundle": "^4.24.7", 17 | "nelmio/cors-bundle": "^2.5", 18 | "phpdocumentor/reflection-docblock": "^5.6.2", 19 | "phpstan/phpdoc-parser": "^1.33", 20 | "symfony/asset": "7.2.*", 21 | "symfony/console": "7.2.*", 22 | "symfony/dotenv": "7.2.*", 23 | "symfony/expression-language": "7.2.*", 24 | "symfony/flex": "^2.7", 25 | "symfony/framework-bundle": "7.2.*", 26 | "symfony/mailer": "7.2.*", 27 | "symfony/property-access": "7.2.*", 28 | "symfony/property-info": "7.2.*", 29 | "symfony/runtime": "7.2.*", 30 | "symfony/security-bundle": "7.2.*", 31 | "symfony/serializer": "7.2.*", 32 | "symfony/twig-bundle": "7.2.*", 33 | "symfony/validator": "7.2.*", 34 | "symfony/yaml": "7.2.*", 35 | "twig/extra-bundle": "^2.12|^3.21", 36 | "twig/twig": "^2.12|^3.21.1" 37 | }, 38 | "require-dev": { 39 | "dama/doctrine-test-bundle": "^8.3", 40 | "doctrine/doctrine-fixtures-bundle": "^3.7.1", 41 | "orbitale/array-fixture": "^1.3.6", 42 | "phpunit/phpunit": "^10.5.46", 43 | "pierstoval/smoke-testing": "^1.2.1", 44 | "symfony/browser-kit": "7.2.*", 45 | "symfony/css-selector": "7.2.*", 46 | "symfony/dom-crawler": "7.2.*", 47 | "symfony/phpunit-bridge": "^7.2.6", 48 | "symfony/var-dumper": "7.2.*", 49 | "symfony/web-profiler-bundle": "7.2.*" 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "php-http/discovery": true, 54 | "symfony/flex": true, 55 | "symfony/runtime": true 56 | }, 57 | "bump-after-update": true, 58 | "sort-packages": true 59 | }, 60 | "autoload": { 61 | "psr-4": { 62 | "App\\": "src/" 63 | } 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "App\\Tests\\": "tests/" 68 | } 69 | }, 70 | "replace": { 71 | "symfony/polyfill-ctype": "*", 72 | "symfony/polyfill-iconv": "*", 73 | "symfony/polyfill-php72": "*", 74 | "symfony/polyfill-php73": "*", 75 | "symfony/polyfill-php74": "*", 76 | "symfony/polyfill-php80": "*", 77 | "symfony/polyfill-php81": "*", 78 | "symfony/polyfill-php82": "*" 79 | }, 80 | "scripts": { 81 | "auto-scripts": { 82 | "cache:clear": "symfony-cmd", 83 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 84 | }, 85 | "post-install-cmd": [ 86 | "@auto-scripts" 87 | ], 88 | "post-update-cmd": [ 89 | "@auto-scripts" 90 | ] 91 | }, 92 | "conflict": { 93 | "symfony/symfony": "*" 94 | }, 95 | "extra": { 96 | "symfony": { 97 | "allow-contrib": true, 98 | "require": "7.2.*", 99 | "docker": true 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /backend/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 6 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 10 | ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], 11 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 12 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], 13 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 14 | Pierstoval\SmokeTesting\SmokeTestingBundle::class => ['dev' => true, 'test' => true], 15 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 16 | EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true], 17 | Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], 18 | ]; 19 | -------------------------------------------------------------------------------- /backend/config/packages/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | title: API 3 | version: 1.0.0 4 | defaults: 5 | stateless: true 6 | cache_headers: 7 | vary: ['Content-Type', 'Authorization', 'Origin'] 8 | -------------------------------------------------------------------------------- /backend/config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /backend/config/packages/csrf.yaml: -------------------------------------------------------------------------------- 1 | # Enable stateless CSRF protection for forms and logins/logouts 2 | framework: 3 | form: 4 | csrf_protection: 5 | token_id: submit 6 | 7 | csrf_protection: 8 | stateless_token_ids: 9 | - submit 10 | - authenticate 11 | - logout 12 | -------------------------------------------------------------------------------- /backend/config/packages/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | when@test: 2 | dama_doctrine_test: 3 | enable_static_connection: true 4 | enable_static_meta_data_cache: true 5 | enable_static_query_cache: true 6 | -------------------------------------------------------------------------------- /backend/config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | use_savepoints: true 5 | 6 | # IMPORTANT: You MUST configure your server version, 7 | # either here or in the DATABASE_URL env var (see .env file) 8 | #server_version: '16' 9 | 10 | profiling_collect_backtrace: '%kernel.debug%' 11 | orm: 12 | auto_generate_proxy_classes: true 13 | enable_lazy_ghost_objects: true 14 | report_fields_where_declared: true 15 | validate_xml_mapping: true 16 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 17 | identity_generation_preferences: 18 | Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity 19 | auto_mapping: true 20 | mappings: 21 | App: 22 | type: attribute 23 | is_bundle: false 24 | dir: '%kernel.project_dir%/src/Entity' 25 | prefix: 'App\Entity' 26 | alias: App 27 | controller_resolver: 28 | auto_mapping: false 29 | 30 | when@test: 31 | doctrine: 32 | dbal: 33 | # "TEST_TOKEN" is typically set by ParaTest 34 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 35 | 36 | when@prod: 37 | doctrine: 38 | orm: 39 | auto_generate_proxy_classes: false 40 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 41 | query_cache_driver: 42 | type: pool 43 | pool: doctrine.system_cache_pool 44 | result_cache_driver: 45 | type: pool 46 | pool: doctrine.result_cache_pool 47 | 48 | framework: 49 | cache: 50 | pools: 51 | doctrine.result_cache_pool: 52 | adapter: cache.app 53 | doctrine.system_cache_pool: 54 | adapter: cache.system 55 | -------------------------------------------------------------------------------- /backend/config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: false 7 | -------------------------------------------------------------------------------- /backend/config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | 5 | # Note that the session will be started ONLY if you read or write from it. 6 | session: true 7 | 8 | #esi: true 9 | #fragments: true 10 | 11 | when@test: 12 | framework: 13 | test: true 14 | session: 15 | storage_factory_id: session.storage.factory.mock_file 16 | -------------------------------------------------------------------------------- /backend/config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /backend/config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] 5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 6 | allow_headers: ['Content-Type', 'Authorization'] 7 | expose_headers: ['Link'] 8 | max_age: 3600 9 | paths: 10 | '^/': null 11 | -------------------------------------------------------------------------------- /backend/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /backend/config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 3 | password_hashers: 4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 5 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 6 | providers: 7 | users_in_memory: { memory: null } 8 | firewalls: 9 | dev: 10 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 11 | security: false 12 | main: 13 | lazy: true 14 | provider: users_in_memory 15 | 16 | # activate different ways to authenticate 17 | # https://symfony.com/doc/current/security.html#the-firewall 18 | 19 | # https://symfony.com/doc/current/security/impersonating_user.html 20 | # switch_user: true 21 | 22 | # Easy way to control access for large sections of your site 23 | # Note: Only the *first* access control that matches will be used 24 | access_control: 25 | # - { path: ^/admin, roles: ROLE_ADMIN } 26 | # - { path: ^/profile, roles: ROLE_USER } 27 | 28 | when@test: 29 | security: 30 | password_hashers: 31 | # By default, password hashers are resource intensive and take time. This is 32 | # important to generate secure password hashes. In tests however, secure hashes 33 | # are not important, waste resources and increase test times. The following 34 | # reduces the work factor to the lowest possible values. 35 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 36 | algorithm: auto 37 | cost: 4 # Lowest possible value for bcrypt 38 | time_cost: 3 # Lowest possible value for argon 39 | memory_cost: 10 # Lowest possible value for argon 40 | -------------------------------------------------------------------------------- /backend/config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: en 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - en 7 | providers: 8 | -------------------------------------------------------------------------------- /backend/config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | file_name_pattern: '*.twig' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /backend/config/packages/twig_component.yaml: -------------------------------------------------------------------------------- 1 | twig_component: 2 | anonymous_template_directory: 'components/' 3 | defaults: 4 | # Namespace & directory for components 5 | App\Twig\Components\: 'components/' 6 | -------------------------------------------------------------------------------- /backend/config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # Enables validator auto-mapping support. 4 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 5 | #auto_mapping: 6 | # App\Entity\: [] 7 | 8 | when@test: 9 | framework: 10 | validation: 11 | not_compromised_password: false 12 | -------------------------------------------------------------------------------- /backend/config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /backend/config/preload.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE books (id VARCHAR(36) NOT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | $this->addSql('DROP TABLE books'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /backend/public/index.php: -------------------------------------------------------------------------------- 1 | redirect($this->adminUrlGenerator->setController(BookCrudController::class)->generateUrl()); 25 | } 26 | 27 | public function configureDashboard(): Dashboard 28 | { 29 | return Dashboard::new() 30 | ->setTitle('Book store'); 31 | } 32 | 33 | public function configureMenuItems(): iterable 34 | { 35 | yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); 36 | yield MenuItem::linkToCrud('Books', 'fas fa-list', Book::class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/DataFixtures/BookFixtures.php: -------------------------------------------------------------------------------- 1 | self::BOOK_ID, 'title' => 'Test book']; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pierstoval/php-js-boilerplate/23ca0dab084407b8fa469da9a56b62b4d5422529/backend/src/Entity/.gitignore -------------------------------------------------------------------------------- /backend/src/Entity/Book.php: -------------------------------------------------------------------------------- 1 | id = (string) Uuid::v7(); 22 | } 23 | 24 | public function getId(): string 25 | { 26 | return $this->id; 27 | } 28 | 29 | public function getTitle(): string 30 | { 31 | return $this->title; 32 | } 33 | 34 | public function setTitle(?string $title): void 35 | { 36 | $this->title = $title ?: ''; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/Kernel.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | 7 | {% block stylesheets %} 8 | {{ encore_entry_link_tags('app') }} 9 | {% endblock %} 10 | 11 | {% block javascripts %} 12 | {{ encore_entry_script_tags('app') }} 13 | {% endblock %} 14 | 15 | 16 | {% block body %}{% endblock %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /backend/tests/Admin/AdminBooksTest.php: -------------------------------------------------------------------------------- 1 | runFunctionalTest( 18 | $this->getTestDataForActionAndCrudController('index', BookCrudController::class) 19 | ->expectStatusCode(200) 20 | ->appendCallableExpectation(fn () => $this->assertListHasXElements(1)) 21 | ->appendCallableExpectation(fn () => $this->assertElementXIdentifierIs(0, BookFixtures::BOOK_ID)) 22 | ->appendCallableExpectation(fn () => $this->assertElementXFieldIs(0, 'Title', 'Test book')) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/tests/Admin/AdminListTestUtils.php: -------------------------------------------------------------------------------- 1 | crawler = $crawler; 15 | } 16 | 17 | private function getCrawler(): Crawler 18 | { 19 | if (!isset($this->crawler)) { 20 | throw new \RuntimeException('Crawler has not been set.'."\n".'You must set it in the tester via this kind of code:'."\n".'$testData->appendCallableExpectation(fn($_, Crawler $crawler) => $this->setCrawler($crawler))'); 21 | } 22 | 23 | return $this->crawler; 24 | } 25 | 26 | protected function assertListHasXElements(int $numberOfElements): void 27 | { 28 | $this->assertCount($numberOfElements, $this->getTableList()); 29 | } 30 | 31 | protected function assertElementXIdentifierIs(int $elementIndex, string $expectedId): void 32 | { 33 | $this->assertSame($expectedId, $this->getTableList()->eq($elementIndex)->attr('data-id')); 34 | } 35 | 36 | protected function assertElementXFieldIs(int $elementIndex, string $fieldName, string $expectedValue): void 37 | { 38 | $this->assertSame($expectedValue, 39 | $this->getTableList() 40 | ->eq($elementIndex) 41 | ->filter(\sprintf('td[data-label="%s"]', $fieldName)) 42 | ->text() 43 | ); 44 | } 45 | 46 | protected function getTableList(): Crawler 47 | { 48 | return $this->getCrawler()->filter('#main table.datagrid tbody tr'); 49 | } 50 | 51 | protected function getTestDataForActionAndCrudController(string $action, $crudController): FunctionalTestData 52 | { 53 | $url = \sprintf('/admin?crudAction=%s&crudControllerFqcn=%s', $action, $crudController); 54 | 55 | return FunctionalTestData::withUrl($url) 56 | ->appendCallableExpectation(fn ($_, Crawler $crawler) => $this->setCrawler($crawler)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/tests/AllRoutesTest.php: -------------------------------------------------------------------------------- 1 | runFunctionalTest( 16 | FunctionalTestData::withUrl('/api/books/96ba14a6-f7d3-42fb-b874-548d4d0f55c5') 17 | ->expectStatusCode(200) 18 | ->expectJsonParts([ 19 | 'id' => '96ba14a6-f7d3-42fb-b874-548d4d0f55c5', 20 | 'title' => 'Test book', 21 | ]) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 9 | } 10 | 11 | if ($_SERVER['APP_DEBUG']) { 12 | umask(0000); 13 | } 14 | -------------------------------------------------------------------------------- /backend/translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pierstoval/php-js-boilerplate/23ca0dab084407b8fa469da9a56b62b4d5422529/backend/translations/.gitignore -------------------------------------------------------------------------------- /compose.override.yaml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | php: 4 | ports: 5 | - "2019:2019" # Caddy API 6 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | include: 2 | - backend/compose.yaml 3 | 4 | services: 5 | php: 6 | build: 7 | context: ./docker/php/ 8 | working_dir: /app/ 9 | volumes: 10 | - ./:/app 11 | - caddy_data:/data 12 | - caddy_config:/config 13 | ports: 14 | - "80:80" 15 | - "443:443" 16 | - "443:443/udp" 17 | 18 | mailcatcher: 19 | image: mailhog/mailhog 20 | ports: ['8025:8025'] 21 | 22 | node: 23 | build: ./docker/node/ 24 | working_dir: /srv/ 25 | ports: 26 | - "3000:3000" 27 | volumes: 28 | - ./frontend/:/srv 29 | 30 | volumes: 31 | caddy_data: 32 | caddy_config: 33 | -------------------------------------------------------------------------------- /docker/mailcatcher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:alpine 2 | 3 | RUN apk add --no-cache \ 4 | sqlite-libs 5 | 6 | RUN apk add --no-cache --virtual .build-deps \ 7 | build-base \ 8 | sqlite-dev \ 9 | && gem install mailcatcher --no-document \ 10 | && apk del .build-deps 11 | 12 | EXPOSE 1080 13 | EXPOSE 1025 14 | 15 | CMD ["--smtp-port", "1025", "--http-port", "1080", "--ip", "0.0.0.0", "-f"] 16 | ENTRYPOINT ["mailcatcher"] 17 | -------------------------------------------------------------------------------- /docker/node/Dockerfile: -------------------------------------------------------------------------------- 1 | # Using ubuntu because playwright isn't compatible with debian. 2 | FROM ubuntu:22.04 3 | 4 | ENV GOSU_VERSION=1.17 \ 5 | NODE_VERSION=22 \ 6 | RUN_USER="node" 7 | 8 | COPY bin/entrypoint.sh /bin/entrypoint 9 | 10 | WORKDIR /srv 11 | 12 | ENTRYPOINT ["/bin/entrypoint"] 13 | 14 | RUN export DEBIAN_FRONTEND=noninteractive \ 15 | && set -xe \ 16 | && apt-get update \ 17 | && apt-get upgrade -y curl \ 18 | && chmod a+x /bin/entrypoint \ 19 | \ 20 | \ 21 | && `# Install Node.js` \ 22 | && (curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -) \ 23 | && apt-get install -y nodejs \ 24 | && npm -g i npm \ 25 | && npm -g i pnpm \ 26 | \ 27 | \ 28 | && `# User management for entrypoint` \ 29 | && curl -L -s -o /bin/gosu https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-$(dpkg --print-architecture | awk -F- '{ print $NF }') \ 30 | && chmod +x /bin/gosu \ 31 | && mkdir -p /home \ 32 | && groupadd ${RUN_USER} \ 33 | && adduser --home=/home --shell=/bin/bash --ingroup=${RUN_USER} --disabled-password --quiet --gecos "" --force-badname ${RUN_USER} \ 34 | && chown ${RUN_USER}:${RUN_USER} /home \ 35 | \ 36 | \ 37 | && `# E2E Testing` \ 38 | && pnpx playwright install-deps \ 39 | && runuser -l ${RUN_USER} -c 'pnpx playwright install' \ 40 | \ 41 | \ 42 | && `# Clean apt and remove unused libs/packages to make image smaller` \ 43 | && npm cache clean --force \ 44 | && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false \ 45 | && apt-get -y autoremove \ 46 | && apt-get clean \ 47 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/www/* /var/cache/* /home/.composer/cache 48 | 49 | CMD ["pnpm", "run", "dev", "--port", "3000", "--host", "0.0.0.0"] 50 | -------------------------------------------------------------------------------- /docker/node/bin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | uid=$(stat -c %u /srv) 5 | gid=$(stat -c %g /srv) 6 | 7 | sed -i -r "s/${RUN_USER}:x:\d+:\d+:/${RUN_USER}:x:$uid:$gid:/g" /etc/passwd 8 | sed -i -r "s/${RUN_USER}:x:\d+:/${RUN_USER}:x:$gid:/g" /etc/group 9 | 10 | chown -R "${RUN_USER}:${RUN_USER}" /srv/ 11 | 12 | # If dependencies are not installed, install them 13 | [ -d node_modules ] || pnpm install 14 | 15 | if [ $# -eq 0 ]; then 16 | printf "\033[32m[%s]\033[0m %s\n" "Node" "Please run a command" 17 | exit 1 18 | else 19 | exec gosu "${RUN_USER}" "$@" 20 | fi 21 | -------------------------------------------------------------------------------- /docker/php/Caddyfile: -------------------------------------------------------------------------------- 1 | # The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server. 2 | # 3 | # https://frankenphp.dev/docs/config 4 | # https://caddyserver.com/docs/caddyfile 5 | 6 | { 7 | skip_install_trust 8 | 9 | #debug 10 | 11 | {$CADDY_GLOBAL_OPTIONS} 12 | 13 | frankenphp { 14 | {$FRANKENPHP_CONFIG} 15 | } 16 | } 17 | 18 | {$CADDY_EXTRA_CONFIG} 19 | 20 | {$SERVER_NAME:localhost} { 21 | #log { 22 | # # Redact the authorization query parameter that can be set by Mercure 23 | # format filter { 24 | # request>uri query { 25 | # replace authorization REDACTED 26 | # } 27 | # } 28 | #} 29 | 30 | root /app/backend/public/ 31 | encode zstd br gzip 32 | 33 | # Uncomment the following lines to enable Mercure and Vulcain modules 34 | #mercure { 35 | # # Transport to use (default to Bolt) 36 | # transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db} 37 | # # Publisher JWT key 38 | # publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG} 39 | # # Subscriber JWT key 40 | # subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG} 41 | # # Allow anonymous subscribers (double-check that it's what you want) 42 | # anonymous 43 | # # Enable the subscription API (double-check that it's what you want) 44 | # subscriptions 45 | # # Extra directives 46 | # {$MERCURE_EXTRA_DIRECTIVES} 47 | #} 48 | #vulcain 49 | 50 | {$CADDY_SERVER_EXTRA_DIRECTIVES} 51 | 52 | @file_exists `file({path}) && !path_regexp(".php")` 53 | 54 | @php `path_regexp('(.php|^/(api|admin|_profiler|_wdt|_error))')` 55 | 56 | @websockets { 57 | header Connection *Upgrade* 58 | header Upgrade websocket 59 | } 60 | 61 | handle / { 62 | reverse_proxy node:3000 63 | } 64 | 65 | # Proxy websockets to node container 66 | handle @websockets { 67 | reverse_proxy node:3000 68 | } 69 | 70 | # Load files that exist already in "backend/public/" 71 | handle @file_exists { 72 | file_server 73 | } 74 | 75 | # If file does not exist, or contains ".php", proxy to PHP 76 | handle @php { 77 | php_server 78 | } 79 | 80 | # Fall back to node.js application 81 | handle { 82 | reverse_proxy node:3000 83 | } 84 | } 85 | 86 | # As an alternative to editing the above site block, you can add your own site 87 | # block files in the Caddyfile.d directory, and they will be included as long 88 | # as they use the .caddyfile extension. 89 | 90 | import Caddyfile.d/*.caddyfile 91 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dunglas/frankenphp:php8.4 2 | 3 | ENV PANTHER_NO_SANDBOX=1 \ 4 | PATH=/home/.composer/vendor/bin:$PATH \ 5 | PATH=/home/.config/composer/vendor/bin:$PATH 6 | 7 | LABEL maintainer="pierstoval@gmail.com" 8 | 9 | COPY --from=composer /usr/bin/composer /usr/bin/composer 10 | 11 | RUN \ 12 | set -xe \ 13 | && apt-get update \ 14 | && apt-get upgrade -y curl \ 15 | curl \ 16 | unzip \ 17 | \ 18 | && install-php-extensions \ 19 | apcu \ 20 | curl \ 21 | intl \ 22 | mbstring \ 23 | pdo_pgsql \ 24 | opcache \ 25 | xml \ 26 | zip \ 27 | xdebug \ 28 | \ 29 | && `# Static analysis` \ 30 | && curl -L https://cs.symfony.com/download/php-cs-fixer-v3.phar -o /usr/local/bin/php-cs-fixer && chmod a+x /usr/local/bin/php-cs-fixer 31 | 32 | COPY ./Caddyfile /etc/frankenphp/Caddyfile 33 | 34 | COPY etc/php.ini /usr/local/etc/php/conf.d/99-application.ini 35 | -------------------------------------------------------------------------------- /docker/php/bin/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | uid=$(stat -c %u /srv) 5 | gid=$(stat -c %g /srv) 6 | 7 | sed -i "s/user = .*/user = ${RUN_USER}/g" "/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" 8 | sed -i "s/group = .*/group = ${RUN_USER}/g" "/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" 9 | sed -i -r "s/${RUN_USER}:x:\d+:\d+:/${RUN_USER}:x:$uid:$gid:/g" /etc/passwd 10 | sed -i -r "s/${RUN_USER}:x:\d+:/${RUN_USER}:x:$gid:/g" /etc/group 11 | 12 | chown -R "${RUN_USER}:${RUN_USER}" "${HOME}" 13 | chown -R "${RUN_USER}:${RUN_USER}" "/srv/" 14 | chown -R "${RUN_USER}:${RUN_USER}" "/run/php" 15 | find /var/log/ -iname "*php*" -type f -exec chown -R "${RUN_USER}:${RUN_USER}" {} \; 16 | 17 | if [ $# -eq 0 ]; then 18 | echo "Please run a command." 19 | exit 1 20 | else 21 | exec gosu "${RUN_USER}" "$@" 22 | fi 23 | -------------------------------------------------------------------------------- /docker/php/etc/php.ini: -------------------------------------------------------------------------------- 1 | allow_url_include = off 2 | assert.active = on 3 | date.timezone = Europe/Paris 4 | max_execution_time = 180 5 | memory_limit = 1024M 6 | phar.readonly = off 7 | post_max_size = 100M 8 | precision = 17 9 | realpath_cache_size = 4M 10 | realpath_cache_ttl = 3600 11 | serialize_precision = -1 12 | session.use_strict_mode = On 13 | short_open_tag = off 14 | upload_max_filesize = 100M 15 | zend.detect_unicode = Off 16 | 17 | [assert] 18 | zend_assertions = 1 19 | assert.exception = 1 20 | 21 | [apcu] 22 | apc.enable_cli = 1 23 | apc.enabled = 1 24 | apc.shm_size = 128M 25 | apc.ttl = 7200 26 | 27 | [errors] 28 | display_errors = On 29 | display_startup_errors = off 30 | error_reporting = E_ALL 31 | 32 | [opcache] 33 | opcache.enable = 1 34 | opcache.enable_cli = 1 35 | opcache.max_accelerated_files = 50000 36 | 37 | [xdebug] 38 | xdebug.mode = develop 39 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | test-results 2 | node_modules 3 | .pnpm-store 4 | 5 | # Output 6 | .output 7 | .vercel 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | 25 | /src/lib/openapi 26 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/e2e/demo.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected nav link', async ({ page }) => { 4 | await page.goto('/'); 5 | expect(await page.textContent('nav a')).toBe('Home'); 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import ts from 'typescript-eslint'; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte'], 23 | 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser 27 | } 28 | } 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/'] 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/orval.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'orval'; 2 | 3 | export default defineConfig({ 4 | applicationApi: { 5 | input: './build/openapi.json', 6 | output: { 7 | mode: 'split', 8 | clean: true, 9 | prettier: true, 10 | tslint: true, 11 | target: './src/lib/openapi/index.ts', 12 | schemas: 'src/lib/openapi/model/', 13 | client: 'axios' // This is the default. 14 | }, 15 | hooks: { 16 | afterAllFilesWrite: 'prettier --write' 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "vite build", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 10 | "dev": "vite dev", 11 | "format": "prettier --write .", 12 | "lint": "eslint . && prettier --check .", 13 | "orval": "orval", 14 | "playwright": "playwright", 15 | "postinstall": "svelte-kit sync", 16 | "preview": "vite preview", 17 | "test:unit": "vitest", 18 | "test:e2e": "playwright test", 19 | "test": "pnpm run test:e2e && pnpm run test:unit --run" 20 | }, 21 | "devDependencies": { 22 | "@playwright/test": "^1.49.0", 23 | "@sveltejs/adapter-static": "^3.0.6", 24 | "@sveltejs/kit": "^2.9.0", 25 | "@sveltejs/vite-plugin-svelte": "^5.0.1", 26 | "@testing-library/jest-dom": "^6.6.3", 27 | "@testing-library/svelte": "^5.2.6", 28 | "@types/eslint": "^9.6.1", 29 | "axios": "^1.7.8", 30 | "eslint": "^9.16.0", 31 | "eslint-config-prettier": "^9.1.0", 32 | "eslint-plugin-svelte": "^2.46.1", 33 | "globals": "^15.13.0", 34 | "jsdom": "^25.0.1", 35 | "orval": "^7.3.0", 36 | "prettier": "^3.4.1", 37 | "prettier-plugin-svelte": "^3.3.2", 38 | "sass": "^1.81.0", 39 | "svelte": "^5.3.1", 40 | "svelte-check": "^4.1.0", 41 | "typescript": "^5.7.2", 42 | "typescript-eslint": "^8.16.0", 43 | "vite": "^6.0.2", 44 | "vitest": "^2.1.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'pnpm run build && pnpm run preview', 6 | port: 4173 7 | }, 8 | use: { 9 | browserName: 'firefox', 10 | defaultBrowserType: 'firefox', 11 | headless: true 12 | }, 13 | testDir: 'e2e' 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/app.scss: -------------------------------------------------------------------------------- 1 | * { 2 | border: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/src/lib/components/TopMenu.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /frontend/src/lib/components/TopMenu.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { render } from '@testing-library/svelte'; 3 | import '@testing-library/jest-dom'; 4 | import ComponentToTest from './TopMenu.svelte'; 5 | 6 | describe('TopMenu component', () => { 7 | it('can be instantiated', async () => { 8 | const rendered = render(ComponentToTest); 9 | 10 | // const element = rendered.container; 11 | // expect(element).toBeDefined(); 12 | // 13 | // const link = rendered.container.querySelector('li a'); 14 | // expect(link).toBeDefined(); 15 | // expect(link.innerText).toStrictEqual('Home'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /frontend/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 22 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const trailingSlash = 'always'; 2 | -------------------------------------------------------------------------------- /frontend/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

Homepage

2 |

It's working!

3 | 4 | 9 | -------------------------------------------------------------------------------- /frontend/src/routes/books/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |

Available books:

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {#each books as book (book.id)} 28 | 29 | 30 | 31 | 34 | 35 | {:else} 36 | 37 | 40 | 41 | {/each} 42 | 43 |
#TitleActions
{book.id.substring(0, 8)}{book.id.length > 8 ? '…' : ''}{book.title} 32 | View 33 |
38 | No books yet! Use the admin to add some: {host} 39 |
44 | 45 | 68 | -------------------------------------------------------------------------------- /frontend/src/routes/books/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | {#if book === null} 24 | {#if finished_loading} 25 | No book found... 26 | {:else} 27 | Loading... 28 | {/if} 29 | {:else} 30 |

Book: {book.title}

31 | 32 | Id: {book.id} 33 | {/if} 34 | -------------------------------------------------------------------------------- /frontend/src/routes/books/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | /** @type {import('./$types').PageLoad} */ 4 | export function load({ params }) { 5 | if (!params.id) { 6 | throw error(404, 'Not found'); 7 | } 8 | 9 | return { 10 | id: params.id 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pierstoval/php-js-boilerplate/23ca0dab084407b8fa469da9a56b62b4d5422529/frontend/static/favicon.ico -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pierstoval/php-js-boilerplate/23ca0dab084407b8fa469da9a56b62b4d5422529/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter({ 15 | pages: './build/', 16 | assets: './build/', 17 | precompress: true, 18 | fallback: 'index.html' 19 | }), 20 | prerender: { 21 | crawl: true 22 | } 23 | } 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { configDefaults } from 'vitest/config'; 4 | import { svelteTesting } from '@testing-library/svelte/vite'; 5 | 6 | export default defineConfig({ 7 | plugins: [sveltekit(), svelteTesting()], 8 | 9 | css: { 10 | preprocessorOptions: { 11 | scss: { 12 | api: 'modern' 13 | } 14 | } 15 | }, 16 | 17 | test: { 18 | include: ['src/**/*.{test,spec}.{js,ts}'], 19 | exclude: [...configDefaults.exclude, '**/build/**', '**/.svelte-kit/**', '**/dist/**'], 20 | globals: true, 21 | environment: 'jsdom' 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /update_dependencies.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | CWD=$(realpath "$(dirname "${BASH_SOURCE[0]}")") 6 | 7 | cd "$CWD" 8 | 9 | info() { 10 | printf " %s" "$1" 11 | } 12 | err() { 13 | printf " \033[31m[ERROR]\033[0m %s\n" "$1" 14 | } 15 | ok() { 16 | printf " \033[32m%s\033[0m\n" "Done!" 17 | } 18 | 19 | info "Remove dependencies" 20 | rm -rf frontend/node_modules backend/vendor 21 | ok 22 | 23 | info "Update frontend dependencies" 24 | pnpm --dir=frontend upgrade --latest 25 | ok 26 | 27 | info "Update backend dependencies" 28 | composer --working-dir=backend update --with-all-dependencies --no-scripts --no-interaction 29 | ok 30 | 31 | --------------------------------------------------------------------------------