├── .github └── workflows │ └── build.yml ├── .gitignore ├── .phpstorm.meta.php ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── bootstrap.php └── config.php ├── appveyor.yml ├── bin └── learnyouphp ├── composer.json ├── composer.lock ├── docker-compose.yml ├── exercises ├── array-we-go │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── baby-steps │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── concerned-about-separation │ ├── problem │ │ └── problem.md │ └── solution │ │ ├── DirectoryFilter.php │ │ └── solution.php ├── database-read │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── dependency-heaven │ ├── problem │ │ └── problem.md │ └── solution │ │ ├── composer.json │ │ └── solution.php ├── exceptional-coding │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── filtered-ls │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── hello-world │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── http-json-api │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php ├── my-first-io │ ├── problem │ │ └── problem.md │ └── solution │ │ └── solution.php └── time-server │ ├── problem │ └── problem.md │ └── solution │ └── solution.php ├── phpstan-bootstrap.php ├── phpstan.neon ├── phpunit.xml ├── src └── Exercise │ ├── ArrayWeGo.php │ ├── BabySteps.php │ ├── ConcernedAboutSeparation.php │ ├── DatabaseRead.php │ ├── DependencyHeaven.php │ ├── ExceptionalCoding.php │ ├── FilteredLs.php │ ├── HelloWorld.php │ ├── HttpJsonApi.php │ ├── MyFirstIo.php │ └── TimeServer.php └── test ├── Exercise ├── ArrayWeGoTest.php ├── BabyStepsTest.php ├── ConcernedAboutSeparationTest.php ├── DatabaseReadTest.php ├── DependencyHeavenTest.php ├── ExceptionalCodingTest.php ├── FilteredLsTest.php ├── HelloWorldTest.php ├── HttpJsonApiTest.php ├── MyFirstIoTest.php └── TimeServerTest.php ├── bootstrap.php ├── res ├── concerned-about-separation │ ├── include.php │ └── no-include.php └── time-server │ ├── no-server.php │ ├── solution-wrong.php │ └── solution.php └── solutions └── dependency-heaven ├── correct-solution ├── composer.json └── solution.php ├── no-code ├── composer.json └── solution.php ├── no-composer └── solution.php └── wrong-endpoint ├── composer.json └── solution.php /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: LearnYouPhp 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | php: [8.0, 8.1, 8.2, 8.3] 16 | 17 | name: PHP ${{ matrix.php }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | tools: composer:v2,pecl 27 | extensions: pdo_sqlite 28 | 29 | - name: Install Dependencies 30 | run: composer update 31 | 32 | - name: Run phpunit tests 33 | run: | 34 | mkdir -p build/logs 35 | vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml 36 | 37 | - name: Run phpcs 38 | run: composer cs 39 | 40 | - name: Run phpstan 41 | run: composer static 42 | 43 | - name: Install with workshop-manager (PR) 44 | if: github.ref != 'refs/heads/master' 45 | run: | 46 | curl -O https://php-school.github.io/workshop-manager/workshop-manager.phar 47 | chmod +x workshop-manager.phar 48 | ./workshop-manager.phar install learnyouphp ${{ github.head_ref }} ${{github.event.pull_request.head.repo.html_url}} 49 | ./workshop-manager.phar installed 50 | 51 | - name: Install with workshop-manager (push) 52 | if: github.ref == 'refs/heads/master' 53 | run: | 54 | curl -O https://php-school.github.io/workshop-manager/workshop-manager.phar 55 | chmod +x workshop-manager.phar 56 | ./workshop-manager.phar install learnyouphp master 57 | ./workshop-manager.phar installed 58 | 59 | - name: Coverage upload 60 | if: matrix.php == '7.3' 61 | run: bash <(curl -s https://codecov.io/bash) 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor 3 | /.idea 4 | program.php 5 | program.js 6 | /solution 7 | exercises/*/solution/vendor 8 | exercises/*/solution/composer.lock 9 | /test/solutions/*/*/vendor 10 | /test/solutions/*/*/composer.lock 11 | .phpunit.result.cache 12 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | [ 6 | "" == "@", 7 | ], 8 | \Interop\Container\ContainerInterface::get('') => [ 9 | "" == "@", 10 | ], 11 | ]; 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 4 | 5 | ## [Unreleased][unreleased] 6 | ### Added 7 | 8 | ### Changed 9 | 10 | ### Fixed 11 | 12 | ### Removed 13 | 14 | ## [0.6.0] 15 | ### Added 16 | - Support for PHP 7.4 & PHP 8.0 17 | - Modernised code base (static analysis, type hints, phpunit, update dependencies) 18 | 19 | ### Removed 20 | - Support for PHP 7.1 and lower 21 | 22 | ## [0.5.1] 23 | ### Fixed 24 | - Fix dev dependency issue 25 | 26 | ## [0.5.0] 27 | ### Fixed 28 | - Updated klein/klein to fix output comparison which causes an exercise to incorrectly fail (#84) 29 | 30 | ### Changed 31 | - Updated to php-school/php-workshop 2.2 32 | - Updated to phpunit/phpunit 5.7 33 | 34 | ## [0.4.0] 35 | ### Changed 36 | - Updated to php-school/php-workshop 2.0 (#72) 37 | 38 | ## [0.3.3] 39 | ### Fixed 40 | - Clarified getting started instructions and automatically create first file for students (#70) 41 | 42 | ## [0.3.2] 43 | ### Fixed 44 | - Fix counting of new lines in exercise 3 (#64) 45 | 46 | ### Changed 47 | - Upgraded hoa/socket to 1.0 (#65) 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # PHP School - Learn You PHP 3 | # A revolutionary new way to learn PHP. 4 | # Bring your imagination to life in an open learning eco-system. 5 | # http://phpschool.io 6 | # 7 | 8 | FROM phpschool/workshop-manager 9 | MAINTAINER Michael Woodward 10 | 11 | RUN workshop-manager install learnyouphp 12 | 13 | RUN echo "\ 14 | ----------------------------------\n\ 15 | Welcome to Learn You PHP! \n\ 16 | To get started run the command: learnyouphp \n\ 17 | ----------------------------------" > /etc/motd 18 | RUN echo "cat /etc/motd" >> ~/.bashrc 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Aydin Hassan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Learn You PHP!

2 | 3 |

4 | The very first PHP School workshop. A revolutionary new way to learn PHP
Bring your imagination to life in an open learning eco-system 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | --- 20 | 21 |

22 | Learn You PHP Workshop 23 |

24 | 25 | ## Installation 26 | 27 | 1. Install [PHP](http://php.net/downloads.php) 28 | 2. Install [workshop-manager](https://www.phpschool.io/) 29 | 3. Run `workshop-manager install learnyouphp` 30 | 4. Run `learnyouphp` 31 | 32 | **learnyouphp** will run through a series of PHP workshops. Starting at a basic "Hello World" and moving on to more advanced exercises about dealing with filesystems, objects, exceptions and streams. 33 | -------------------------------------------------------------------------------- /app/bootstrap.php: -------------------------------------------------------------------------------- 1 | addExercise(HelloWorld::class); 39 | $app->addExercise(BabySteps::class); 40 | $app->addExercise(MyFirstIo::class); 41 | $app->addExercise(FilteredLs::class); 42 | $app->addExercise(ConcernedAboutSeparation::class); 43 | $app->addExercise(ArrayWeGo::class); 44 | $app->addExercise(ExceptionalCoding::class); 45 | $app->addExercise(DatabaseRead::class); 46 | $app->addExercise(TimeServer::class); 47 | $app->addExercise(HttpJsonApi::class); 48 | $app->addExercise(DependencyHeaven::class); 49 | 50 | $art = <<setLogo($art); 61 | $app->setFgColour('magenta'); 62 | $app->setBgColour('black'); 63 | 64 | return $app; 65 | -------------------------------------------------------------------------------- /app/config.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/../', 25 | 26 | //Exercises 27 | BabySteps::class => create(BabySteps::class), 28 | HelloWorld::class => create(HelloWorld::class), 29 | HttpJsonApi::class => create(HttpJsonApi::class), 30 | MyFirstIo::class => factory(function (ContainerInterface $c) { 31 | return new MyFirstIo($c->get(Filesystem::class), FakerFactory::create()); 32 | }), 33 | FilteredLs::class => factory(function (ContainerInterface $c) { 34 | return new FilteredLs($c->get(Filesystem::class)); 35 | }), 36 | ConcernedAboutSeparation::class => factory(function (ContainerInterface $c) { 37 | return new ConcernedAboutSeparation( 38 | $c->get(Filesystem::class), 39 | $c->get(Parser::class) 40 | ); 41 | }), 42 | ArrayWeGo::class => factory(function (ContainerInterface $c) { 43 | return new ArrayWeGo($c->get(Filesystem::class), FakerFactory::create()); 44 | }), 45 | ExceptionalCoding::class => factory(function (ContainerInterface $c) { 46 | return new ExceptionalCoding($c->get(Filesystem::class), FakerFactory::create()); 47 | }), 48 | DatabaseRead::class => factory(function (ContainerInterface $c) { 49 | return new DatabaseRead(FakerFactory::create()); 50 | }), 51 | TimeServer::class => factory(function (ContainerInterface $c) { 52 | return new TimeServer(); 53 | }), 54 | DependencyHeaven::class => factory(function (ContainerInterface $c) { 55 | return new DependencyHeaven(FakerFactory::create('fr_FR')); 56 | }), 57 | 58 | 'eventListeners' => [ 59 | 'create-solution-for-first-exercise' => [ 60 | 'exercise.selected.hello-world' => [ 61 | DI\value(function () { 62 | if (!file_exists(getcwd() . '/hello-world.php')) { 63 | touch(getcwd() . '/hello-world.php'); 64 | } 65 | }) 66 | ] 67 | ] 68 | ] 69 | ]; 70 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | platform: 'x86' 3 | clone_folder: C:\projects\my-php-workshop 4 | branches: 5 | except: 6 | - gh-pages 7 | 8 | init: 9 | - SET COMPOSER_NO_INTERACTION=1 10 | 11 | install: 12 | - SET PATH=C:\Program Files\OpenSSL;%PATH% 13 | - cinst php 14 | - cd c:\tools\php 15 | - copy php.ini-production php.ini 16 | - echo date.timezone="UTC" >> php.ini 17 | - echo extension_dir=ext >> php.ini 18 | - echo extension=php_openssl.dll >> php.ini 19 | - echo extension=php_mbstring.dll >> php.ini 20 | - echo extension=php_sockets.dll >> php.ini 21 | - SET PATH=C:\tools\php;%PATH% 22 | - cd C:\projects\my-php-workshop 23 | - php -r "readfile('http://getcomposer.org/installer');" | php 24 | - php composer.phar install --prefer-source --no-progress 25 | 26 | test_script: 27 | - ps: cd C:\projects\my-php-workshop 28 | - ps: gl 29 | - vendor\bin\phpunit.bat 30 | -------------------------------------------------------------------------------- /bin/learnyouphp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run()); 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-school/learn-you-php", 3 | "description": "An introduction to PHP's core features: i/o, http, arrays, exceptions and so on.", 4 | "keywords": ["cli", "console", "terminal", "phpschool", "php-school", "workshop", "learning", "education", "php"], 5 | "homepage": "https://www.phpschool.io", 6 | "license": "MIT", 7 | "type": "php-school-workshop", 8 | "authors": [ 9 | { 10 | "name": "Aydin Hassan", 11 | "email": "aydin@hotmail.co.uk" 12 | } 13 | ], 14 | "require" : { 15 | "php" : ">=8.0", 16 | "ext-pdo_sqlite": "*", 17 | "php-school/php-workshop": "dev-master", 18 | "ext-sockets": "*" 19 | }, 20 | "require-dev": { 21 | "phpstan/phpstan": "^1.9", 22 | "squizlabs/php_codesniffer": "^3.5", 23 | "phpunit/phpunit": "^9.6" 24 | }, 25 | "autoload" : { 26 | "psr-4" : { 27 | "PhpSchool\\LearnYouPhp\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { "PhpSchool\\LearnYouPhpTest\\": "test/" } 32 | }, 33 | "bin": ["bin/learnyouphp"], 34 | "scripts" : { 35 | "test": [ 36 | "@unit-tests", 37 | "@cs", 38 | "@static" 39 | ], 40 | "unit-tests": "phpunit", 41 | "cs" : [ 42 | "phpcs src --standard=PSR12", 43 | "phpcs test --standard=PSR12 --ignore='test/solutions'" ], 44 | "cs-fix" : [ 45 | "phpcbf src --standard=PSR12 --encoding=UTF-8", 46 | "phpcbf test --standard=PSR12 --encoding=UTF-8 --ignore='test/solutions'" 47 | ], 48 | "static": "phpstan --ansi analyse --level max src" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | workshop: 5 | container_name: phpschool-learnyouphp 6 | image: phpschool/learn-you-php 7 | volumes: 8 | - ~/.phpschool-save.json:/root/.phpschool-save.json 9 | - $PHPSCHOOL_CODE_DIR/learnyouphp:/phpschool:rw 10 | stdin_open: true 11 | tty: true 12 | -------------------------------------------------------------------------------- /exercises/array-we-go/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that takes an array of filepaths as arguments, filtering out files that do not exist and mapping existing files to `SplFileObject`'s. 2 | 3 | Finally output the basename of the files, each on a new line. 4 | 5 | The full path of the files to read will be provided as command line arguments. You do not need to make your own test files. 6 | 7 | ---------------------------------------------------------------------- 8 | ## HINTS 9 | 10 | Remember, the first argument will be the programs file path and not an argument passed to the program. 11 | 12 | You will be expected to make use of core array functions, `array_shift`, `array_filter` and `array_map`. 13 | 14 | To check a file exists you will need to use `file_exists($filePath)`. This method will *return* a *boolean* `true` or `false`. 15 | 16 | {{ doc 'SplFileObject' en class.splfileobject.php }} 17 | 18 | ---------------------------------------------------------------------- 19 | -------------------------------------------------------------------------------- /exercises/array-we-go/solution/solution.php: -------------------------------------------------------------------------------- 1 | getBasename()); 13 | } 14 | -------------------------------------------------------------------------------- /exercises/baby-steps/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that accepts one or more numbers as command-line arguments and prints the sum of those numbers to the console (stdout). 2 | 3 | ---------------------------------------------------------------------- 4 | ## HINTS 5 | 6 | You can access command-line arguments via the global `$argv` array. 7 | 8 | To get started, write a program that simply contains: 9 | 10 | ```php 11 | 33 | string(7) "program.php" 34 | [1] => 35 | string(1) "1" 36 | [2] => 37 | string(1) "2" 38 | [3] => 39 | string(1) "3" 40 | } 41 | ``` 42 | 43 | You'll need to think about how to loop through the number of arguments so you can output just their sum. The first element of the `$argv` array is always the name of your script. eg `solution.php`, so you need to start at the 2nd element (index 1), adding each item to the total until you reach the end of the array. 44 | 45 | Also be aware that all elements of `$argv` are strings and you may need to *coerce* them into numbers. You can do this by prefixing the property with a cast `(int)` or just adding them. PHP will coerce it for you. 46 | 47 | {{ cli }} 48 | {{ appname }} will be supplying arguments to your program when you run `{{ appname }} verify program.php` so you don't need to supply them yourself. To test your program without verifying it, you can invoke it with `{{ appname }} run program.php`. When you use `run`, you are invoking the test environment that {{ appname }} sets up for each exercise. 49 | {{ cli }} 50 | 51 | {{ cloud }} 52 | PHP school will be supplying arguments to your program when you click `Verify` in the editor. To test your program without verifying it, you can just execute it by pressing the `Run` button in the editor. 53 | {{ cloud }} 54 | ---------------------------------------------------------------------- 55 | -------------------------------------------------------------------------------- /exercises/baby-steps/solution/solution.php: -------------------------------------------------------------------------------- 1 | getFiles($argv[1], $argv[2])); -------------------------------------------------------------------------------- /exercises/database-read/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that receives a database connection string (DSN). Connect to the database, query it and update some data. 2 | 3 | Display the information of all the users in the database table `users` whose age is over 30. Print out each row on a new line formatted like: 4 | 5 | `User: Jim Morrison Age: 27 Sex: male` 6 | 7 | Finally, you will be given a random name as the second argument to your program, you should update the row in the `users` table which corresponds to this name. You should change the name to `David Attenborough`. 8 | 9 | ---------------------------------------------------------------------- 10 | ## HINTS 11 | 12 | This is an exercise introducing databases and PDO. PDO is a powerful abstraction library for dealing with different database vendors in a consistent manner. You can read the PDO manual here: 13 | 14 | [http://php.net/manual/en/book.pdo.php](http://php.net/manual/en/book.pdo.php) 15 | 16 | A short introduction can be found here: 17 | 18 | [http://www.phptherightway.com/#pdo_extension](http://www.phptherightway.com/#pdo_extension) 19 | 20 | The most interesting class will be `\PDO`. The first parameter is the DSN string. The second and third are the username and password for the database. They are not needed for this exercise and can be left out. 21 | 22 | The `users` table is structured as follows: 23 | 24 | ``` 25 | +----+-----------------+-----+--------+ 26 | | id | name | age | gender | 27 | +----+-----------------+-----+--------+ 28 | | 1 | Mark Corrigan | 32 | male | 29 | | 2 | Jeremy Usbourne | 30 | male | 30 | +----+-----------------+-----+--------+ 31 | ``` 32 | 33 | The table will be pre-populated with random data. 34 | 35 | In order to get the data you will most likely want the `query` method. Which you can pass an SQL statement to. `query` returns an instance of `PDOStatement` which you can iterate over in a foreach loop, like so: 36 | 37 | ```php 38 | query('SELECT * FROM users') as $row) { 40 | } 41 | ``` 42 | 43 | `$row` is now an array of data. The key will be the column name and the value is the database value. 44 | 45 | 46 | You should use prepared statements to perform the updating. You should be most interested in the `prepare` and `execute` methods. 47 | 48 | Remember, the first argument will be the program's file path and not an argument passed to the program. 49 | 50 | ---------------------------------------------------------------------- 51 | -------------------------------------------------------------------------------- /exercises/database-read/solution/solution.php: -------------------------------------------------------------------------------- 1 | query('SELECT * FROM users WHERE age > 30'); 5 | foreach ($users as $user) { 6 | echo "User: {$user['name']} Age: {$user['age']} Sex: {$user['gender']}\n"; 7 | } 8 | $nameToUpdate = $argv[2]; 9 | $stmt = $db->prepare('UPDATE users SET name = :newName WHERE name = :oldName'); 10 | $stmt->execute([':newName' => 'David Attenborough', ':oldName' => $nameToUpdate]); -------------------------------------------------------------------------------- /exercises/dependency-heaven/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write an HTTP **server** that serves JSON data when it receives a POST request to `/reverse`, `/snake` and `/titleize`. 2 | 3 | The POST data will contain a single parameter `data` which you will need to manipulate depending on the endpoint. 4 | 5 | ### /reverse 6 | 7 | A request with `data = "PHP School is awesome!"` should return the response: 8 | 9 | ```json 10 | { 11 | "result": "!emosewa si loohcS PHP" 12 | } 13 | ``` 14 | 15 | ### /snake 16 | 17 | A request with `data = "No, It Really Is..."` should return the response: 18 | 19 | ```json 20 | { 21 | "result": "no_it_really_is" 22 | } 23 | ``` 24 | 25 | ### /titleize 26 | 27 | A request with `data = "you know you love it, don't you?"` should return the response: 28 | 29 | ```json 30 | { 31 | "result": "You Know You Love It, Don't You?" 32 | } 33 | ``` 34 | 35 | You should use the routing library `league/route` for this task, pulling it in as a dependency through Composer. `league/route` allows us to define actions which respond to requests based on the request URI and HTTP method. 36 | 37 | The library works by accepting a PSR7 request and returns to you a PSR7 response. It is up to you to marshal the request object and output the response to the browser. 38 | 39 | There are a few other components we need, in order to use `league/route`: 40 | 41 | * `laminas/laminas-diactoros` - For the PSR7 requests and responses. 42 | * `laminas/laminas-httphandlerrunner` - For outputting the PSR7 response to the browser. 43 | 44 | `laminas/laminas-diactoros` is a PSR7 implementation. PSR's are standards defined by the PHP-FIG, a committee of PHP projects, attempting to increase interoperability in the PHP ecosystem. PSR7 is a standard for modelling HTTP requests. We can use the `laminas/laminas-diactoros` package to marshal a PSR7 request object from the PHP super globals like so: 45 | 46 | ```php 47 | $request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals(); 48 | ``` 49 | 50 | `$request` is now a PSR7 request, that can be used with `league/route`. 51 | 52 | `laminas/laminas-httphandlerrunner` provides a simple method to output the PSR7 response to the browser, handling headers, status codes and the content. Use it like so: 53 | 54 | ```php 55 | (new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); 56 | ``` 57 | 58 | Where `$response` is a PSR7 response. 59 | 60 | In between this, you will need to define your routes and execute the dispatching mechanism to receive a response. Refer to the `league\route` [documentation](https://route.thephpleague.com/5.x/usage/). 61 | 62 | Each route action will be passed the PSR7 request where you can access the request parameters and body. To access the `data` key from the request body, you would use the following: 63 | 64 | ```php 65 | $data = $request->getParsedBody()['data'] ?? ''; 66 | ``` 67 | 68 | In each action, you are expected to return a PSR7 response. `laminas/laminas-diactoros` provides a few ways to accomplish this: 69 | 70 | A text response: 71 | 72 | ```php 73 | $response = new \Laminas\Diactoros\Response('My Content'); 74 | ``` 75 | 76 | A JSON response: 77 | 78 | ```php 79 | $response = (new \Laminas\Diactoros\Response(json_encode(['desert' => 'cookies']))) 80 | ->withAddedHeader('Content-Type', 'application/json; charset=utf-8''); 81 | ``` 82 | 83 | Or you could use the helper class, which takes care of encoding and setting the correct headers: 84 | 85 | ```php 86 | $response = (new \Laminas\Diactoros\Response\JsonResponse(['desert' => 'cookies'])); 87 | ``` 88 | 89 | Finally, you will also be required to use `symfony/string` to manipulate the data as this correctly handles multibyte characters. 90 | 91 | ---------------------------------------------------------------------- 92 | ## HINTS 93 | 94 | {{ cli }} 95 | Point your browser to [https://getcomposer.org/doc/00-intro.md](https://getcomposer.org/doc/00-intro.md) which will walk you through **Installing Composer** if you haven't already! 96 | 97 | Use `composer init` to create your `composer.json` file with interactive search. 98 | {{ cli }} 99 | 100 | {{ cloud }} 101 | Composer is installed and ready to go on cloud, use the `Composer Deps` button in the editor to search for and install your dependencies. While you should read the documentation for [Composer](https://getcomposer.org/doc/00-intro.md), it's important to note that the way we manage dependencies on PHP School cloud, is not how you would manage them in your own projects. We abstract away the `composer.json` file to keep it simple. 102 | {{ cloud }} 103 | 104 | For more details look at the docs for... 105 | 106 | * **Composer** - [https://getcomposer.org/doc/01-basic-usage.md](https://getcomposer.org/doc/01-basic-usage.md) 107 | * **League Route** - [https://route.thephpleague.com/5.x/usage/](https://route.thephpleague.com/5.x/usage/) 108 | * **Symfony String** - [https://symfony.com/doc/current/components/string.html](https://symfony.com/doc/current/components/string.html) 109 | * **PSR** - [https://www.php-fig.org/psr/](https://www.php-fig.org/psr/) 110 | * **PSR 7** - [https://www.php-fig.org/psr/psr-7/](https://www.php-fig.org/psr/psr-7/) 111 | 112 | Oh, and don't forget to use the Composer autoloader with: 113 | 114 | ```php 115 | require_once __DIR__ . '/vendor/autoload.php'; 116 | ``` 117 | ---------------------------------------------------------------------- 118 | -------------------------------------------------------------------------------- /exercises/dependency-heaven/solution/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-you-php/dependency-heaven", 3 | "description": "String manipulation with Composer", 4 | "require": { 5 | "symfony/string": "^5.0 | ^6.0", 6 | "league/route": "^5.1", 7 | "laminas/laminas-diactoros": "^2.4", 8 | "laminas/laminas-httphandlerrunner": "^1.2 | ^2.4" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Michael Woodward", 13 | "email": "mikeymike.mw@gmail.com" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /exercises/dependency-heaven/solution/solution.php: -------------------------------------------------------------------------------- 1 | post('/reverse', function (ServerRequestInterface $request): ResponseInterface { 19 | $str = $request->getParsedBody()['data'] ?? ''; 20 | 21 | return new JsonResponse([ 22 | 'result' => s($str)->reverse()->toString() 23 | ]); 24 | }); 25 | 26 | $router->post('/snake', function (ServerRequestInterface $request): ResponseInterface { 27 | $str = $request->getParsedBody()['data'] ?? ''; 28 | 29 | return new JsonResponse([ 30 | 'result' => s($str)->snake()->toString() 31 | ]); 32 | }); 33 | 34 | 35 | $router->post('/titleize', function (ServerRequestInterface $request): ResponseInterface { 36 | $str = $request->getParsedBody()['data'] ?? ''; 37 | 38 | return new JsonResponse([ 39 | 'result' => s($str)->title(true)->toString() 40 | ]); 41 | }); 42 | 43 | $response = $router->dispatch($request); 44 | 45 | (new SapiEmitter())->emit($response); 46 | -------------------------------------------------------------------------------- /exercises/exceptional-coding/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that takes an array of filepaths as arguments and outputs the basename of each, separated by a new line. 2 | 3 | Every file should exist but under exceptional circumstances some files may not. If this occurs, output a message similar to the below. 4 | 5 | ``` 6 | Unable to open file at path '/file/path' 7 | ``` 8 | 9 | The full path of the files to read will be provided as command line arguments. You do not need to make your own test files. 10 | 11 | ---------------------------------------------------------------------- 12 | ## HINTS 13 | 14 | You are urged to use `try... catch` logic here along with the `SplFileObject` contruct which throws a `RuntimeException` when a file does not exist. 15 | 16 | {{ doc SplFileObject en class.splfileobject.php }} 17 | 18 | ---------------------------------------------------------------------- 19 | -------------------------------------------------------------------------------- /exercises/exceptional-coding/solution/solution.php: -------------------------------------------------------------------------------- 1 | getBasename()); 10 | } catch (RuntimeException $e) { 11 | echo sprintf("Unable to open file at path '%s'\n", $filePath); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /exercises/filtered-ls/problem/problem.md: -------------------------------------------------------------------------------- 1 | Create a program that prints a list of files in a given directory, filtered by the extension of the files. You will be provided a directory name as the first argument to your program (e.g. `/path/to/dir/`) and a file extension to filter by as the second argument. 2 | 3 | For example, if you get `txt` as the second argument then you will need to filter the list to only files that **end with .txt**. Note that the second argument _will not_ come prefixed with a full stop (`.`). 4 | 5 | The list of files should be printed out, one file per line. 6 | 7 | ---------------------------------------------------------------------- 8 | ## HINTS 9 | 10 | The `DirectoryIterator` class takes a pathname as its first argument. 11 | 12 | Using an iterator in a `foreach` loop will provide you with a `SplFileInfo` object for each file. 13 | 14 | ```php 15 | getExtension() === $argv[2]) { 5 | echo $file->getFilename() . "\n"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /exercises/hello-world/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that prints the text "Hello World" to the console (stdout). 2 | 3 | ---------------------------------------------------------------------- 4 | ## HINTS 5 | {{ cli }} 6 | To make a PHP program, create a new file with a `.php` extension and start writing PHP! Execute your program by running it with the 7 | `php` command. e.g.: 8 | 9 | ```sh 10 | $ php program.php 11 | ``` 12 | {{ cli }} 13 | 14 | {{ cloud }} 15 | 16 | We've created you an empty file, look in the file tree for `solution.php`. That's your starting point. 17 | 18 | We'll execute your solution file for you when you press the `Run` or `Verify` buttons. The `Run` button simply runs your program and captures the output, displaying it for you to view. The `Verify` button runs your program but performs some extra tasks, such as comparing the output, checking for certain structures and function calls, etc. It then displays the result of those verifications. 19 | 20 | Both `Run` and `Verify` execute your program with random inputs which are determined by the current exercise. For example one exercise might generate a bunch of numbers to pass to your program, where another one might pass you a JSON encoded string. 21 | 22 | {{ cloud }} 23 | 24 | You can write to the console from a PHP program with the following code: 25 | 26 | ```php 27 | format('U');`. It can also parse this format if you pass the string into the `\DateTime` constructor. The various parameters to `format()` will also 46 | come in handy. 47 | 48 | {{ doc DateTime en class.datetime.php}} 49 | 50 | ---------------------------------------------------------------------- 51 | -------------------------------------------------------------------------------- /exercises/http-json-api/solution/solution.php: -------------------------------------------------------------------------------- 1 | $date->format('H'), 13 | "minute" => $date->format('i'), 14 | "second" => $date->format('s') 15 | ]); 16 | exit; 17 | } 18 | } 19 | 20 | if ($urlParts['path'] === '/api/unixtime') { 21 | if (isset($_GET['iso'])) { 22 | $date = new \DateTime($_GET['iso']); 23 | echo json_encode(["unixtime" => $date->format('U')]); 24 | exit; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /exercises/my-first-io/problem/problem.md: -------------------------------------------------------------------------------- 1 | Write a program that uses a single filesystem operation to read a file and print the number of newlines `\n` it contains to the console (stdout), similar to running `cat file | wc -l` in a terminal. 2 | 3 | The full path to the file to read will be provided as the first command-line argument. You do not need to make your own test file. 4 | 5 | ---------------------------------------------------------------------- 6 | ## HINTS 7 | 8 | To perform a filesystem operation you can use the global PHP functions. 9 | 10 | To read a file, you'll need to use `file_get_contents('/path/to/file')`. This method will *return* a `string` containing the complete contents of the file. 11 | 12 | {{ doc file_get_contents en function.file-get-contents.php }} 13 | 14 | If you're looking for an easy way to count the number of newlines in a string, the PHP function `substr_count` has your back, it can be used to count the number of substring occurrences. 15 | 16 | ---------------------------------------------------------------------- -------------------------------------------------------------------------------- /exercises/my-first-io/solution/solution.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i:s') . "\n"; 8 | socket_write($clientSock, $date, strlen($date)); 9 | socket_close($clientSock); 10 | socket_close($sock); 11 | -------------------------------------------------------------------------------- /phpstan-bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./src 7 | 8 | 9 | 10 | ./test 11 | ./test/solutions 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Exercise/ArrayWeGo.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 27 | $this->faker = $faker; 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return 'Array We Go!'; 33 | } 34 | 35 | public function getDescription(): string 36 | { 37 | return 'Filter an array of file paths and map to SplFile objects'; 38 | } 39 | 40 | /** 41 | * @inheritdoc 42 | */ 43 | public function getArgs(): array 44 | { 45 | $this->filesystem->mkdir($this->getTemporaryPath()); 46 | 47 | $fileCount = rand(2, 10); 48 | $realFiles = rand(1, $fileCount - 1); 49 | 50 | $files = []; 51 | foreach (range(1, $fileCount) as $index) { 52 | $file = sprintf('%s/%s.txt', $this->getTemporaryPath(), $this->faker->uuid()); 53 | if ($index <= $realFiles) { 54 | $this->filesystem->touch($file); 55 | } 56 | $files[] = $file; 57 | } 58 | 59 | return [$files]; 60 | } 61 | 62 | public function tearDown(): void 63 | { 64 | $this->filesystem->remove($this->getTemporaryPath()); 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function getRequiredFunctions(): array 71 | { 72 | return ['array_shift', 'array_filter', 'array_map']; 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function getBannedFunctions(): array 79 | { 80 | return ['basename']; 81 | } 82 | 83 | public function getType(): ExerciseType 84 | { 85 | return new ExerciseType(ExerciseType::CLI); 86 | } 87 | 88 | public function configure(ExerciseDispatcher $dispatcher): void 89 | { 90 | $dispatcher->requireCheck(FunctionRequirementsCheck::class); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Exercise/BabySteps.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 32 | $this->parser = $parser; 33 | } 34 | 35 | public function getName(): string 36 | { 37 | return 'Concerned about Separation?'; 38 | } 39 | 40 | public function getDescription(): string 41 | { 42 | return 'Separate code and utilise files and classes'; 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getArgs(): array 49 | { 50 | $folder = $this->getTemporaryPath(); 51 | 52 | $files = [ 53 | "learnyouphp.dat", 54 | "learnyouphp.txt", 55 | "learnyouphp.sql", 56 | "txt", 57 | "sql", 58 | "api.html", 59 | "html", 60 | "README.md", 61 | "CHANGELOG.md", 62 | "LICENCE.md", 63 | "md", 64 | "data.json", 65 | "json", 66 | "data.dat", 67 | "words.dat", 68 | "w00t.dat", 69 | "w00t.txt", 70 | "wrrrrongdat", 71 | "dat", 72 | ]; 73 | 74 | $this->filesystem->mkdir($folder); 75 | array_walk($files, function ($file) use ($folder) { 76 | $this->filesystem->dumpFile(sprintf('%s/%s', $folder, $file), ''); 77 | }); 78 | 79 | $ext = ''; 80 | while ($ext === '') { 81 | $index = array_rand($files); 82 | $ext = pathinfo($files[$index], PATHINFO_EXTENSION); 83 | } 84 | 85 | return [[$folder, $ext]]; 86 | } 87 | 88 | public function getSolution(): SolutionInterface 89 | { 90 | return DirectorySolution::fromDirectory(__DIR__ . '/../../exercises/concerned-about-separation/solution'); 91 | } 92 | 93 | public function tearDown(): void 94 | { 95 | $this->filesystem->remove($this->getTemporaryPath()); 96 | } 97 | 98 | public function check(Input $input): ResultInterface 99 | { 100 | $statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program'))); 101 | 102 | if (null === $statements) { 103 | return Failure::fromNameAndReason($this->getName(), 'No code was found'); 104 | } 105 | 106 | $include = null; 107 | foreach ($statements as $statement) { 108 | if ($statement instanceof Expression && $statement->expr instanceof Include_) { 109 | $include = $statement; 110 | break; 111 | } 112 | } 113 | 114 | if (null === $include) { 115 | return Failure::fromNameAndReason($this->getName(), 'No require statement found'); 116 | } 117 | 118 | return new Success($this->getName()); 119 | } 120 | 121 | public function getType(): ExerciseType 122 | { 123 | return new ExerciseType(ExerciseType::CLI); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Exercise/DatabaseRead.php: -------------------------------------------------------------------------------- 1 | faker = $faker; 31 | } 32 | 33 | public function getName(): string 34 | { 35 | return 'Database Read'; 36 | } 37 | 38 | public function getDescription(): string 39 | { 40 | return 'Read an SQL databases contents'; 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function getArgs(): array 47 | { 48 | return [[$this->randomRecord['name']]]; 49 | } 50 | 51 | public function seed(PDO $db): void 52 | { 53 | $db 54 | ->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)'); 55 | $stmt = $db->prepare('INSERT INTO users (name, age, gender) VALUES (:name, :age, :gender)'); 56 | 57 | if ($stmt == false) { 58 | return; 59 | } 60 | 61 | $names = []; 62 | for ($i = 0; $i < $this->faker->numberBetween(5, 15); $i++) { 63 | $name = $this->faker->name(); 64 | $age = rand(18, 90); 65 | $gender = rand(0, 100) % 2 ? 'male' : 'female'; 66 | 67 | $stmt->execute([':name' => $name, ':age' => $age, ':gender' => $gender]); 68 | $names[(int) $db->lastInsertId()] = $name; 69 | } 70 | 71 | $randomId = (int) array_rand($names); 72 | $this->randomRecord = ['id' => $randomId, 'name' => (string) $names[$randomId]]; 73 | } 74 | 75 | public function verify(PDO $db): bool 76 | { 77 | $sql = 'SELECT name FROM users WHERE id = :id'; 78 | $stmt = $db->prepare($sql); 79 | 80 | if ($stmt == false) { 81 | return false; 82 | } 83 | 84 | $stmt->execute([':id' => $this->randomRecord['id']]); 85 | $result = $stmt->fetchColumn(); 86 | 87 | return $result === 'David Attenborough'; 88 | } 89 | 90 | public function getType(): ExerciseType 91 | { 92 | return new ExerciseType(ExerciseType::CLI); 93 | } 94 | 95 | public function configure(ExerciseDispatcher $dispatcher): void 96 | { 97 | $dispatcher->requireCheck(DatabaseCheck::class); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Exercise/DependencyHeaven.php: -------------------------------------------------------------------------------- 1 | faker = $faker; 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return 'Dependency Heaven'; 33 | } 34 | 35 | public function getDescription(): string 36 | { 37 | return 'An introduction to Composer dependency management'; 38 | } 39 | 40 | public function getSolution(): SolutionInterface 41 | { 42 | return DirectorySolution::fromDirectory(__DIR__ . '/../../exercises/dependency-heaven/solution'); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function getRequests(): array 49 | { 50 | $requests = []; 51 | 52 | for ($i = 0; $i < rand(2, 5); $i++) { 53 | $requests[] = $this->newApiRequest('/reverse'); 54 | $requests[] = $this->newApiRequest('/snake'); 55 | $requests[] = $this->newApiRequest('/titleize'); 56 | } 57 | 58 | return $requests; 59 | } 60 | 61 | private function newApiRequest(string $endpoint): RequestInterface 62 | { 63 | $request = (new Request('POST', $endpoint)) 64 | ->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 65 | 66 | $request->getBody()->write( 67 | http_build_query(['data' => $this->faker->sentence(rand(3, 6))]) 68 | ); 69 | 70 | return $request; 71 | } 72 | 73 | /** 74 | * @inheritdoc 75 | */ 76 | public function getRequiredPackages(): array 77 | { 78 | return [ 79 | 'league/route', 80 | 'laminas/laminas-diactoros', 81 | 'laminas/laminas-httphandlerrunner', 82 | 'symfony/string' 83 | ]; 84 | } 85 | 86 | public function getType(): ExerciseType 87 | { 88 | return new ExerciseType(ExerciseType::CGI); 89 | } 90 | 91 | public function configure(ExerciseDispatcher $dispatcher): void 92 | { 93 | $dispatcher->requireCheck(ComposerCheck::class); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Exercise/ExceptionalCoding.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 29 | $this->faker = $faker; 30 | } 31 | 32 | public function getName(): string 33 | { 34 | return "Exceptional Coding"; 35 | } 36 | 37 | public function getDescription(): string 38 | { 39 | return "Introduction to Exceptions"; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function getArgs(): array 46 | { 47 | $this->filesystem->mkdir($this->getTemporaryPath()); 48 | 49 | $fileCount = rand(2, 10); 50 | $realFiles = rand(1, $fileCount - 1); 51 | 52 | $files = []; 53 | foreach (range(1, $fileCount) as $index) { 54 | $file = sprintf('%s/%s.txt', $this->getTemporaryPath(), $this->faker->uuid()); 55 | if ($index <= $realFiles) { 56 | $this->filesystem->touch($file); 57 | } 58 | $files[] = $file; 59 | } 60 | 61 | return [$files]; 62 | } 63 | 64 | public function tearDown(): void 65 | { 66 | $this->filesystem->remove($this->getTemporaryPath()); 67 | } 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | public function getRequiredFunctions(): array 73 | { 74 | return []; 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function getBannedFunctions(): array 81 | { 82 | return ['array_filter', 'file_exists']; 83 | } 84 | 85 | public function getType(): ExerciseType 86 | { 87 | return new ExerciseType(ExerciseType::CLI); 88 | } 89 | 90 | public function configure(ExerciseDispatcher $dispatcher): void 91 | { 92 | $dispatcher->requireCheck(FunctionRequirementsCheck::class); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Exercise/FilteredLs.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 21 | } 22 | 23 | public function getName(): string 24 | { 25 | return 'Filtered LS'; 26 | } 27 | 28 | public function getDescription(): string 29 | { 30 | return 'Read files in a folder and filter by a given extension'; 31 | } 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | public function getArgs(): array 37 | { 38 | $folder = $this->getTemporaryPath(); 39 | 40 | $files = [ 41 | "learnyouphp.dat", 42 | "learnyouphp.txt", 43 | "learnyouphp.sql", 44 | "txt", 45 | "sql", 46 | "api.html", 47 | "html", 48 | "README.md", 49 | "CHANGELOG.md", 50 | "LICENCE.md", 51 | "md", 52 | "data.json", 53 | "json", 54 | "data.dat", 55 | "words.dat", 56 | "w00t.dat", 57 | "w00t.txt", 58 | "wrrrrongdat", 59 | "dat", 60 | ]; 61 | 62 | $this->filesystem->mkdir($folder); 63 | array_walk($files, function ($file) use ($folder) { 64 | $this->filesystem->dumpFile(sprintf('%s/%s', $folder, $file), ''); 65 | }); 66 | 67 | $ext = ''; 68 | while ($ext === '') { 69 | $index = array_rand($files); 70 | $ext = pathinfo($files[$index], PATHINFO_EXTENSION); 71 | } 72 | 73 | return [[$folder, $ext]]; 74 | } 75 | 76 | public function tearDown(): void 77 | { 78 | $this->filesystem->remove($this->getTemporaryPath()); 79 | } 80 | 81 | public function getType(): ExerciseType 82 | { 83 | return new ExerciseType(ExerciseType::CLI); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Exercise/HelloWorld.php: -------------------------------------------------------------------------------- 1 | format(DATE_ISO8601))))), 32 | (new Request('GET', sprintf($url, 'unixtime', urlencode((new \DateTime())->format(DATE_ISO8601))))) 33 | ]; 34 | } 35 | 36 | public function getType(): ExerciseType 37 | { 38 | return new ExerciseType(ExerciseType::CGI); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exercise/MyFirstIo.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 29 | $this->faker = $faker; 30 | } 31 | 32 | public function getName(): string 33 | { 34 | return 'My First IO'; 35 | } 36 | 37 | public function getDescription(): string 38 | { 39 | return 'Read a file from the file system'; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function getArgs(): array 46 | { 47 | $path = $this->getTemporaryPath(); 48 | $paragraphs = implode("\n\n", (array) $this->faker->paragraphs(rand(5, 50))); 49 | $this->filesystem->dumpFile($path, $paragraphs); 50 | 51 | return [[$path]]; 52 | } 53 | 54 | public function tearDown(): void 55 | { 56 | $this->filesystem->remove($this->getTemporaryPath()); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function getRequiredFunctions(): array 63 | { 64 | return ['file_get_contents']; 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function getBannedFunctions(): array 71 | { 72 | return ['file']; 73 | } 74 | 75 | public function getType(): ExerciseType 76 | { 77 | return new ExerciseType(ExerciseType::CLI); 78 | } 79 | 80 | public function configure(ExerciseDispatcher $dispatcher): void 81 | { 82 | $dispatcher->requireCheck(FunctionRequirementsCheck::class); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Exercise/TimeServer.php: -------------------------------------------------------------------------------- 1 | getEventDispatcher(); 33 | 34 | $appendArgsListener = function (CliExecuteEvent $event) { 35 | $event->appendArg('127.0.0.1'); 36 | $event->appendArg($this->getRandomPort()); 37 | }; 38 | 39 | $eventDispatcher->listen('cli.verify.reference-execute.pre', $appendArgsListener); 40 | $eventDispatcher->listen('cli.verify.student-execute.pre', $appendArgsListener); 41 | $eventDispatcher->listen('cli.run.student-execute.pre', $appendArgsListener); 42 | 43 | $eventDispatcher->listen('cli.verify.reference.executing', function (CliExecuteEvent $event) { 44 | $args = $event->getArgs()->getArrayCopy(); 45 | 46 | //wait for server to boot 47 | usleep(100000); 48 | 49 | $socket = $this->createSocket(); 50 | socket_connect($socket, $args[0], (int) $args[1]); 51 | socket_read($socket, 2048, PHP_NORMAL_READ); 52 | 53 | //wait for shutdown 54 | usleep(100000); 55 | }); 56 | 57 | $eventDispatcher->insertVerifier('cli.verify.student.executing', function (CliExecuteEvent $event) { 58 | $args = $event->getArgs()->getArrayCopy(); 59 | 60 | //wait for server to boot 61 | usleep(100000); 62 | 63 | $socket = $this->createSocket(); 64 | $connectResult = @socket_connect($socket, $args[0], (int) $args[1]); 65 | 66 | if (!$connectResult) { 67 | return Failure::fromNameAndReason($this->getName(), sprintf( 68 | "Client returns an error (number %d): Connection refused while trying to join tcp://127.0.0.1:%d.", 69 | socket_last_error($socket), 70 | $args[1] 71 | )); 72 | } 73 | 74 | $out = (string) socket_read($socket, 2048, PHP_NORMAL_READ); 75 | 76 | //wait for shutdown 77 | usleep(100000); 78 | 79 | $date = new \DateTime(); 80 | 81 | //match the current date but any seconds 82 | //since we can't mock time in PHP easily 83 | if (!preg_match(sprintf('/^%s:([0-5][0-9]|60)\n$/', $date->format('Y-m-d H:i')), $out)) { 84 | return ComparisonFailure::fromNameAndValues($this->getName(), $date->format("Y-m-d H:i:s\n"), $out); 85 | } 86 | return new Success($this->getName()); 87 | }); 88 | 89 | $eventDispatcher->listen('cli.run.student.executing', function (CliExecuteEvent $event) { 90 | /** @var OutputInterface $output */ 91 | $output = $event->getParameter('output'); 92 | $args = $event->getArgs()->getArrayCopy(); 93 | 94 | //wait for server to boot 95 | usleep(100000); 96 | 97 | $socket = $this->createSocket(); 98 | socket_connect($socket, $args[0], (int) $args[1]); 99 | $out = (string) socket_read($socket, 2048, PHP_NORMAL_READ); 100 | 101 | //wait for shutdown 102 | usleep(100000); 103 | 104 | $output->write($out); 105 | }); 106 | } 107 | 108 | private function getRandomPort(): string 109 | { 110 | return (string) mt_rand(1025, 65535); 111 | } 112 | 113 | public function getType(): ExerciseType 114 | { 115 | return new ExerciseType(ExerciseType::CLI); 116 | } 117 | 118 | /** 119 | * @inheritdoc 120 | */ 121 | public function getArgs(): array 122 | { 123 | return []; 124 | } 125 | 126 | private function createSocket(): Socket 127 | { 128 | $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 129 | 130 | if ($socket === false) { 131 | throw new RuntimeException('Cannot create socket'); 132 | } 133 | 134 | return $socket; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /test/Exercise/ArrayWeGoTest.php: -------------------------------------------------------------------------------- 1 | faker = Factory::create(); 29 | $this->filesystem = new Filesystem(); 30 | } 31 | 32 | public function testArrWeGoExercise(): void 33 | { 34 | $e = new ArrayWeGo($this->filesystem, $this->faker); 35 | $this->assertEquals('Array We Go!', $e->getName()); 36 | $this->assertEquals('Filter an array of file paths and map to SplFile objects', $e->getDescription()); 37 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 38 | 39 | $this->assertFileExists(realpath($e->getProblem())); 40 | } 41 | 42 | public function testGetArgsCreateAtLeastOneExistingFile(): void 43 | { 44 | $e = new ArrayWeGo($this->filesystem, $this->faker); 45 | $args = $e->getArgs()[0]; 46 | 47 | $existingFiles = array_filter($args, 'file_exists'); 48 | 49 | foreach ($existingFiles as $file) { 50 | $this->assertFileExists($file); 51 | } 52 | 53 | $this->assertGreaterThanOrEqual(1, count($existingFiles)); 54 | } 55 | 56 | public function testGetArgsHasAtLeastOneNonExistingFile(): void 57 | { 58 | $e = new ArrayWeGo($this->filesystem, $this->faker); 59 | $args = $e->getArgs()[0]; 60 | 61 | $nonExistingFiles = array_filter($args, function ($arg) { 62 | return !file_exists($arg); 63 | }); 64 | 65 | foreach ($nonExistingFiles as $file) { 66 | $this->assertFileDoesNotExist($file); 67 | } 68 | 69 | $this->assertGreaterThanOrEqual(1, count($nonExistingFiles)); 70 | } 71 | 72 | public function testTearDownRemovesFile(): void 73 | { 74 | $e = new ArrayWeGo($this->filesystem, $this->faker); 75 | $args = $e->getArgs()[0]; 76 | 77 | $existingFiles = array_filter($args, 'file_exists'); 78 | 79 | $this->assertFileExists($existingFiles[0]); 80 | 81 | $e->tearDown(); 82 | 83 | $this->assertFileDoesNotExist($existingFiles[0]); 84 | } 85 | 86 | public function testFunctionRequirements(): void 87 | { 88 | $e = new ArrayWeGo($this->filesystem, $this->faker); 89 | $this->assertEquals(['array_shift', 'array_filter', 'array_map'], $e->getRequiredFunctions()); 90 | $this->assertEquals(['basename'], $e->getBannedFunctions()); 91 | } 92 | 93 | public function testConfigure(): void 94 | { 95 | $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class) 96 | ->disableOriginalConstructor() 97 | ->getMock(); 98 | 99 | $dispatcher 100 | ->expects($this->once()) 101 | ->method('requireCheck') 102 | ->with(FunctionRequirementsCheck::class); 103 | 104 | $e = new ArrayWeGo($this->filesystem, $this->faker); 105 | $e->configure($dispatcher); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Exercise/BabyStepsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Baby Steps', $e->getName()); 15 | $this->assertEquals('Simple Addition', $e->getDescription()); 16 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 17 | 18 | //sometime we don't get any args as number of args is random 19 | //we need some args for code-coverage, so just try again 20 | do { 21 | $args = $e->getArgs()[0]; 22 | } while (empty($args)); 23 | 24 | foreach ($args as $arg) { 25 | $this->assertIsNumeric($arg); 26 | } 27 | 28 | $this->assertFileExists(realpath($e->getProblem())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/Exercise/ConcernedAboutSeparationTest.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(); 30 | $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 31 | } 32 | 33 | public function testConcernedAboutSeparationExercise(): void 34 | { 35 | $e = new ConcernedAboutSeparation($this->filesystem, $this->parser); 36 | $this->assertEquals('Concerned about Separation?', $e->getName()); 37 | $this->assertEquals('Separate code and utilise files and classes', $e->getDescription()); 38 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 39 | 40 | $this->assertFileExists(realpath($e->getProblem())); 41 | } 42 | 43 | public function testGetArgsCreatesFilesAndReturnsRandomExt(): void 44 | { 45 | $e = new ConcernedAboutSeparation($this->filesystem, $this->parser); 46 | $args = $e->getArgs()[0]; 47 | $path = $args[0]; 48 | $this->assertFileExists($path); 49 | 50 | $files = [ 51 | "learnyouphp.dat", 52 | "learnyouphp.txt", 53 | "learnyouphp.sql", 54 | "api.html", 55 | "README.md", 56 | "CHANGELOG.md", 57 | "LICENCE.md", 58 | "md", 59 | "data.json", 60 | "data.dat", 61 | "words.dat", 62 | "w00t.dat", 63 | "w00t.txt", 64 | "wrrrrongdat", 65 | "dat", 66 | ]; 67 | 68 | array_walk($files, function ($file) use ($path) { 69 | $this->assertFileExists(sprintf('%s/%s', $path, $file)); 70 | }); 71 | 72 | $extensions = array_unique(array_map(function ($file) { 73 | return pathinfo($file, PATHINFO_EXTENSION); 74 | }, $files)); 75 | 76 | $this->assertContains($args[1], $extensions); 77 | } 78 | 79 | public function testTearDownRemovesFile(): void 80 | { 81 | $e = new ConcernedAboutSeparation($this->filesystem, $this->parser); 82 | $args = $e->getArgs()[0]; 83 | $path = $args[0]; 84 | $this->assertFileExists($path); 85 | 86 | $e->tearDown(); 87 | 88 | $this->assertFileDoesNotExist($path); 89 | } 90 | 91 | public function testCheckReturnsFailureIfNoIncludeFoundInSolution(): void 92 | { 93 | $e = new ConcernedAboutSeparation($this->filesystem, $this->parser); 94 | $failure = $e->check( 95 | new Input('learnyouphp', ['program' => __DIR__ . '/../res/concerned-about-separation/no-include.php']) 96 | ); 97 | 98 | $this->assertInstanceOf(Failure::class, $failure); 99 | $this->assertEquals('No require statement found', $failure->getReason()); 100 | $this->assertEquals('Concerned about Separation?', $failure->getCheckName()); 101 | } 102 | 103 | public function testCheckReturnsSuccessIfIncludeFound(): void 104 | { 105 | $e = new ConcernedAboutSeparation($this->filesystem, $this->parser); 106 | $success = $e->check( 107 | new Input('learnyouphp', ['program' => __DIR__ . '/../res/concerned-about-separation/include.php']) 108 | ); 109 | 110 | $this->assertInstanceOf(Success::class, $success); 111 | $this->assertEquals('Concerned about Separation?', $success->getCheckName()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/Exercise/DatabaseReadTest.php: -------------------------------------------------------------------------------- 1 | faker = Factory::create(); 25 | } 26 | 27 | public function testDatabaseExercise(): void 28 | { 29 | $e = new DatabaseRead($this->faker); 30 | $this->assertEquals('Database Read', $e->getName()); 31 | $this->assertEquals('Read an SQL databases contents', $e->getDescription()); 32 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 33 | 34 | $this->assertFileExists(realpath($e->getProblem())); 35 | } 36 | 37 | public function testSeedAddsRandomUsersToDatabaseAndStoresRandomIdAndName(): void 38 | { 39 | $db = new PDO('sqlite::memory:'); 40 | $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 41 | $e = new DatabaseRead($this->faker); 42 | 43 | $e->seed($db); 44 | 45 | $args = $e->getArgs()[0]; 46 | $stmt = $db->query('SELECT * FROM users;'); 47 | 48 | $users = $stmt->fetchAll(PDO::FETCH_ASSOC); 49 | $this->assertTrue(count($users) >= 5); 50 | $this->assertIsArray($users); 51 | $this->assertContains($args[0], array_column($users, 'name')); 52 | } 53 | 54 | public function testVerifyReturnsTrueIfRecordExistsWithNameUsingStoredId(): void 55 | { 56 | $db = new PDO('sqlite::memory:'); 57 | $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 58 | $e = new DatabaseRead($this->faker); 59 | 60 | $rp = new \ReflectionProperty(DatabaseRead::class, 'randomRecord'); 61 | $rp->setAccessible(true); 62 | $rp->setValue($e, ['id' => 5]); 63 | 64 | $db 65 | ->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, gender TEXT)'); 66 | $stmt = $db->prepare('INSERT INTO users (id, name, age, gender) VALUES (:id, :name, :age, :gender)'); 67 | $stmt->execute([':id' => 5, ':name' => 'David Attenborough', ':age' => 50, ':gender' => 'Male']); 68 | 69 | $this->assertTrue($e->verify($db)); 70 | } 71 | 72 | public function testConfigure(): void 73 | { 74 | $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class) 75 | ->disableOriginalConstructor() 76 | ->getMock(); 77 | 78 | $dispatcher 79 | ->expects($this->once()) 80 | ->method('requireCheck') 81 | ->with(DatabaseCheck::class); 82 | 83 | $e = new DatabaseRead($this->faker); 84 | $e->configure($dispatcher); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/Exercise/DependencyHeavenTest.php: -------------------------------------------------------------------------------- 1 | faker = Factory::create('Fr_fr'); 31 | parent::setUp(); 32 | } 33 | 34 | public function testDependencyHeavenExercise(): void 35 | { 36 | $e = new DependencyHeaven($this->faker); 37 | $this->assertEquals('Dependency Heaven', $e->getName()); 38 | $this->assertEquals('An introduction to Composer dependency management', $e->getDescription()); 39 | $this->assertEquals(ExerciseType::CGI, $e->getType()); 40 | 41 | $this->assertFileExists(realpath($e->getSolution()->getEntryPoint())); 42 | $this->assertFileExists(realpath($e->getProblem())); 43 | } 44 | 45 | public function testGetRequiredPackages(): void 46 | { 47 | $this->assertSame( 48 | ['league/route', 'laminas/laminas-diactoros', 'laminas/laminas-httphandlerrunner', 'symfony/string'], 49 | (new DependencyHeaven($this->faker))->getRequiredPackages() 50 | ); 51 | } 52 | 53 | public function testGetRequests(): void 54 | { 55 | $e = new DependencyHeaven($this->faker); 56 | 57 | $requests = $e->getRequests(); 58 | 59 | foreach ($requests as $request) { 60 | $this->assertInstanceOf(RequestInterface::class, $request); 61 | $this->assertSame('POST', $request->getMethod()); 62 | $this->assertSame(['application/x-www-form-urlencoded'], $request->getHeader('Content-Type')); 63 | $this->assertNotEmpty($request->getBody()); 64 | } 65 | } 66 | 67 | public function testGetRequestsReturnsMultipleRequestsForEachEndpoint(): void 68 | { 69 | $e = new DependencyHeaven($this->faker); 70 | 71 | $endPoints = array_map(function (RequestInterface $request) { 72 | return $request->getUri()->getPath(); 73 | }, $e->getRequests()); 74 | 75 | $counts = array_count_values($endPoints); 76 | foreach (['/reverse', '/snake', '/titleize'] as $endPoint) { 77 | $this->assertTrue(isset($counts[$endPoint])); 78 | $this->assertGreaterThan(1, $counts[$endPoint]); 79 | } 80 | } 81 | 82 | public function testConfigure(): void 83 | { 84 | $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class) 85 | ->disableOriginalConstructor() 86 | ->getMock(); 87 | 88 | $dispatcher 89 | ->expects($this->once()) 90 | ->method('requireCheck') 91 | ->with(ComposerCheck::class); 92 | 93 | $e = new DependencyHeaven($this->faker); 94 | $e->configure($dispatcher); 95 | } 96 | 97 | public function getExerciseClass(): string 98 | { 99 | return DependencyHeaven::class; 100 | } 101 | 102 | public function getApplication(): Application 103 | { 104 | return require __DIR__ . '/../../app/bootstrap.php'; 105 | } 106 | 107 | public function testWithNoComposerFile(): void 108 | { 109 | $this->runExercise('no-composer/solution.php'); 110 | 111 | $this->assertVerifyWasNotSuccessful(); 112 | $this->assertResultsHasFailureAndMatches( 113 | ComposerFailure::class, 114 | function (ComposerFailure $failure) { 115 | return $failure->getMissingComponent() === 'composer.json'; 116 | } 117 | ); 118 | } 119 | 120 | public function testWithNoCode(): void 121 | { 122 | $this->runExercise('no-code/solution.php'); 123 | 124 | $this->assertVerifyWasNotSuccessful(); 125 | 126 | $this->assertResultsHasFailure(Failure::class, 'No code was found'); 127 | } 128 | 129 | public function testWithWrongEndpoint(): void 130 | { 131 | $this->runExercise('wrong-endpoint/solution.php'); 132 | 133 | $this->assertVerifyWasNotSuccessful(); 134 | 135 | $result = $this->getOutputResult(); 136 | 137 | $reverseRequests = collect($result->getResults()) 138 | ->filter(function (ResultInterface $result) { 139 | return $result->getRequest()->getUri()->getPath() === '/reverse'; 140 | }); 141 | 142 | $this->assertGreaterThan(1, $reverseRequests->count()); 143 | 144 | $fails = collect($result->getResults()) 145 | ->filter(function ($result) { 146 | return $result instanceof GenericFailure; 147 | }); 148 | 149 | $this->assertSame($reverseRequests->count(), $fails->count()); 150 | 151 | $fails->each(function (GenericFailure $failure) { 152 | $this->assertStringContainsString( 153 | 'Uncaught League\Route\Http\Exception\NotFoundException', 154 | $failure->getReason() 155 | ); 156 | }); 157 | } 158 | 159 | public function testWithCorrectSolution(): void 160 | { 161 | $this->runExercise('correct-solution/solution.php'); 162 | 163 | $this->assertVerifyWasSuccessful(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/Exercise/ExceptionalCodingTest.php: -------------------------------------------------------------------------------- 1 | faker = Factory::create(); 29 | $this->filesystem = new Filesystem(); 30 | } 31 | 32 | public function testArrWeGoExercise(): void 33 | { 34 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 35 | $this->assertEquals('Exceptional Coding', $e->getName()); 36 | $this->assertEquals('Introduction to Exceptions', $e->getDescription()); 37 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 38 | 39 | $this->assertFileExists(realpath($e->getProblem())); 40 | } 41 | 42 | public function testGetArgsCreateAtleastOneExistingFile(): void 43 | { 44 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 45 | $args = $e->getArgs()[0]; 46 | 47 | $existingFiles = array_filter($args, 'file_exists'); 48 | 49 | foreach ($existingFiles as $file) { 50 | $this->assertFileExists($file); 51 | } 52 | 53 | $this->assertGreaterThanOrEqual(1, count($existingFiles)); 54 | } 55 | 56 | public function testGetArgsHasAtleastOneNonExistingFile(): void 57 | { 58 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 59 | $args = $e->getArgs()[0]; 60 | 61 | $nonExistingFiles = array_filter($args, function ($arg) { 62 | return !file_exists($arg); 63 | }); 64 | 65 | foreach ($nonExistingFiles as $file) { 66 | $this->assertFileDoesNotExist($file); 67 | } 68 | 69 | $this->assertGreaterThanOrEqual(1, count($nonExistingFiles)); 70 | } 71 | 72 | public function testTearDownRemovesFile(): void 73 | { 74 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 75 | $args = $e->getArgs()[0]; 76 | 77 | $existingFiles = array_filter($args, 'file_exists'); 78 | 79 | $this->assertFileExists($existingFiles[0]); 80 | 81 | $e->tearDown(); 82 | 83 | $this->assertFileDoesNotExist($existingFiles[0]); 84 | } 85 | 86 | public function testFunctionRequirements(): void 87 | { 88 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 89 | $this->assertEquals([], $e->getRequiredFunctions()); 90 | $this->assertEquals(['array_filter', 'file_exists'], $e->getBannedFunctions()); 91 | } 92 | 93 | public function testConfigure(): void 94 | { 95 | $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class) 96 | ->disableOriginalConstructor() 97 | ->getMock(); 98 | 99 | $dispatcher 100 | ->expects($this->once()) 101 | ->method('requireCheck') 102 | ->with(FunctionRequirementsCheck::class); 103 | 104 | $e = new ExceptionalCoding($this->filesystem, $this->faker); 105 | $e->configure($dispatcher); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Exercise/FilteredLsTest.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(); 20 | } 21 | 22 | public function testFilteredLsExercise(): void 23 | { 24 | $e = new FilteredLs($this->filesystem); 25 | $this->assertEquals('Filtered LS', $e->getName()); 26 | $this->assertEquals('Read files in a folder and filter by a given extension', $e->getDescription()); 27 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 28 | 29 | $this->assertFileExists(realpath($e->getProblem())); 30 | } 31 | 32 | public function testGetArgsCreatesFilesAndReturnsRandomExt(): void 33 | { 34 | $e = new FilteredLs($this->filesystem); 35 | $args = $e->getArgs()[0]; 36 | $path = $args[0]; 37 | $this->assertFileExists($path); 38 | 39 | $files = [ 40 | "learnyouphp.dat", 41 | "learnyouphp.txt", 42 | "learnyouphp.sql", 43 | "api.html", 44 | "README.md", 45 | "CHANGELOG.md", 46 | "LICENCE.md", 47 | "md", 48 | "data.json", 49 | "data.dat", 50 | "words.dat", 51 | "w00t.dat", 52 | "w00t.txt", 53 | "wrrrrongdat", 54 | "dat", 55 | ]; 56 | 57 | array_walk($files, function ($file) use ($path) { 58 | $this->assertFileExists(sprintf('%s/%s', $path, $file)); 59 | }); 60 | 61 | $extensions = array_unique(array_map(function ($file) { 62 | return pathinfo($file, PATHINFO_EXTENSION); 63 | }, $files)); 64 | 65 | $this->assertContains($args[1], $extensions); 66 | } 67 | 68 | public function testTearDownRemovesFile(): void 69 | { 70 | $e = new FilteredLs($this->filesystem); 71 | $args = $e->getArgs()[0]; 72 | $path = $args[0]; 73 | $this->assertFileExists($path); 74 | 75 | $e->tearDown(); 76 | 77 | $this->assertFileDoesNotExist($path); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/Exercise/HelloWorldTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Hello World', $e->getName()); 16 | $this->assertEquals('Simple Hello World exercise', $e->getDescription()); 17 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 18 | 19 | $this->assertEquals([], $e->getArgs()[0]); 20 | 21 | $this->assertFileExists(realpath($e->getProblem())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/Exercise/HttpJsonApiTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('HTTP JSON API', $e->getName()); 17 | $this->assertEquals('HTTP JSON API - Servers JSON when it receives a GET request', $e->getDescription()); 18 | $this->assertEquals(ExerciseType::CGI, $e->getType()); 19 | 20 | $requests = $e->getRequests(); 21 | $request1 = $requests[0]; 22 | $request2 = $requests[1]; 23 | 24 | $this->assertInstanceOf(RequestInterface::class, $request1); 25 | $this->assertInstanceOf(RequestInterface::class, $request2); 26 | 27 | $this->assertSame('GET', $request1->getMethod()); 28 | $this->assertSame('GET', $request2->getMethod()); 29 | $this->assertSame('www.time.com', $request1->getUri()->getHost()); 30 | $this->assertSame('www.time.com', $request2->getUri()->getHost()); 31 | $this->assertSame('/api/parsetime', $request1->getUri()->getPath()); 32 | $this->assertSame('/api/unixtime', $request2->getUri()->getPath()); 33 | 34 | $this->assertFileExists(realpath($e->getProblem())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Exercise/MyFirstIoTest.php: -------------------------------------------------------------------------------- 1 | faker = Factory::create(); 30 | $this->filesystem = new Filesystem(); 31 | } 32 | 33 | public function testMyFirstIoExercise(): void 34 | { 35 | $e = new MyFirstIo($this->filesystem, $this->faker); 36 | $this->assertEquals('My First IO', $e->getName()); 37 | $this->assertEquals('Read a file from the file system', $e->getDescription()); 38 | $this->assertEquals(ExerciseType::CLI, $e->getType()); 39 | 40 | $this->assertFileExists(realpath($e->getProblem())); 41 | } 42 | 43 | public function testGetArgsCreatesFileWithRandomContentFromFake(): void 44 | { 45 | $e = new MyFirstIo($this->filesystem, $this->faker); 46 | $args = $e->getArgs()[0]; 47 | $path = $args[0]; 48 | $this->assertFileExists($path); 49 | 50 | $content1 = file_get_contents($path); 51 | unlink($path); 52 | 53 | $args = $e->getArgs()[0]; 54 | $path = $args[0]; 55 | $this->assertFileExists($path); 56 | 57 | $content2 = file_get_contents($path); 58 | $this->assertNotEquals($content1, $content2); 59 | } 60 | 61 | public function testTearDownRemovesFile(): void 62 | { 63 | $e = new MyFirstIo($this->filesystem, $this->faker); 64 | $args = $e->getArgs()[0]; 65 | $path = $args[0]; 66 | $this->assertFileExists($path); 67 | 68 | $e->tearDown(); 69 | 70 | $this->assertFileDoesNotExist($path); 71 | } 72 | 73 | public function testFunctionRequirements(): void 74 | { 75 | $e = new MyFirstIo($this->filesystem, $this->faker); 76 | $this->assertEquals(['file_get_contents'], $e->getRequiredFunctions()); 77 | $this->assertEquals(['file'], $e->getBannedFunctions()); 78 | } 79 | 80 | public function testConfigure(): void 81 | { 82 | $dispatcher = $this->getMockBuilder(ExerciseDispatcher::class) 83 | ->disableOriginalConstructor() 84 | ->getMock(); 85 | 86 | $dispatcher 87 | ->expects($this->once()) 88 | ->method('requireCheck') 89 | ->with(FunctionRequirementsCheck::class); 90 | 91 | $e = new MyFirstIo($this->filesystem, $this->faker); 92 | $e->configure($dispatcher); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/Exercise/TimeServerTest.php: -------------------------------------------------------------------------------- 1 | exercise = new TimeServer(); 42 | $runner = new CliRunner($this->exercise, $eventDispatcher); 43 | 44 | $r = new \ReflectionClass($runner); 45 | $rp = $r->getProperty('requiredChecks'); 46 | $rp->setAccessible(true); 47 | $rp->setValue($runner, []); 48 | 49 | $runnerFactory = $this->createPartialMock(CliRunnerFactory::class, ['create']); 50 | $runnerFactory->method('create')->willReturn($runner); 51 | $runnerManager = new RunnerManager(); 52 | $runnerManager->addFactory($runnerFactory); 53 | $this->exerciseDispatcher = new ExerciseDispatcher( 54 | $runnerManager, 55 | $results, 56 | $eventDispatcher, 57 | new CheckRepository([new PhpLintCheck()]) 58 | ); 59 | } 60 | 61 | public function testGetters(): void 62 | { 63 | $this->assertEquals('Time Server', $this->exercise->getName()); 64 | $this->assertEquals('Build a Time Server!', $this->exercise->getDescription()); 65 | $this->assertEquals(ExerciseType::CLI, $this->exercise->getType()); 66 | 67 | $this->assertFileExists(realpath($this->exercise->getProblem())); 68 | } 69 | 70 | public function testFailureIsReturnedIfCannotConnect(): void 71 | { 72 | $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/no-server.php']); 73 | $results = $this->exerciseDispatcher->verify($this->exercise, $input); 74 | $this->assertCount(2, $results); 75 | 76 | $failure = iterator_to_array($results)[0]; 77 | $this->assertInstanceOf(Failure::class, $failure); 78 | 79 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 80 | $reason = '/^Client returns an error \(number \d+\): No connection could be made because'; 81 | $reason .= ' the target machine actively refused it\.\r\n'; 82 | $reason .= ' while trying to join tcp:\/\/127\.0\.0\.1:\d+\.$/'; 83 | } else { 84 | $reason = '/^Client returns an error \(number \d+\): Connection refused'; 85 | $reason .= ' while trying to join tcp:\/\/127\.0\.0\.1:\d+\.$/'; 86 | } 87 | 88 | $this->assertMatchesRegularExpression($reason, $failure->getReason()); 89 | $this->assertEquals('Time Server', $failure->getCheckName()); 90 | } 91 | 92 | public function testFailureIsReturnedIfOutputWasNotCorrect(): void 93 | { 94 | $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution-wrong.php']); 95 | $results = $this->exerciseDispatcher->verify($this->exercise, $input); 96 | 97 | $this->assertCount(2, $results); 98 | $failure = iterator_to_array($results)[0]; 99 | 100 | $this->assertInstanceOf(ComparisonFailure::class, $failure); 101 | $this->assertNotEquals($failure->getExpectedValue(), $failure->getActualValue()); 102 | $this->assertEquals('Time Server', $failure->getCheckName()); 103 | } 104 | 105 | public function testSuccessIsReturnedIfOutputIsCorrect(): void 106 | { 107 | $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution.php']); 108 | $results = $this->exerciseDispatcher->verify($this->exercise, $input); 109 | 110 | $this->assertCount(2, $results); 111 | $success = iterator_to_array($results)[0]; 112 | $this->assertInstanceOf(Success::class, $success); 113 | } 114 | 115 | public function testRun(): void 116 | { 117 | $color = new Color(); 118 | $color->setForceStyle(true); 119 | $output = new StdOutput($color, $terminal = $this->createMock(Terminal::class)); 120 | 121 | $outputRegEx = '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'; 122 | $outputRegEx .= "\n/"; 123 | $this->expectOutputRegex($outputRegEx); 124 | 125 | $input = new Input('learnyouphp', ['program' => __DIR__ . '/../res/time-server/solution.php']); 126 | $this->exerciseDispatcher->run($this->exercise, $input, $output); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i') . "\n"; 11 | socket_write($clientSock, $date, strlen($date)); 12 | socket_close($clientSock); 13 | socket_close($sock); 14 | -------------------------------------------------------------------------------- /test/res/time-server/solution.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i:s') . "\n"; 10 | 11 | socket_write($clientSock, $date, strlen($date)); 12 | socket_close($clientSock); 13 | socket_close($sock); 14 | -------------------------------------------------------------------------------- /test/solutions/dependency-heaven/correct-solution/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-you-php/dependency-heaven", 3 | "description": "An introduction to Composer dependency management", 4 | "require": { 5 | "symfony/string": "^5.0 | ^6.0", 6 | "league/route": "^5.1", 7 | "laminas/laminas-diactoros": "^2.4", 8 | "laminas/laminas-httphandlerrunner": "^1.2 | ^2.4" 9 | } 10 | } -------------------------------------------------------------------------------- /test/solutions/dependency-heaven/correct-solution/solution.php: -------------------------------------------------------------------------------- 1 | post('/reverse', function (ServerRequestInterface $request): ResponseInterface { 19 | $str = $request->getParsedBody()['data'] ?? ''; 20 | 21 | return new JsonResponse([ 22 | 'result' => s($str)->reverse()->toString() 23 | ]); 24 | }); 25 | 26 | $router->post('/snake', function (ServerRequestInterface $request): ResponseInterface { 27 | $str = $request->getParsedBody()['data'] ?? ''; 28 | 29 | return new JsonResponse([ 30 | 'result' => s($str)->snake()->toString() 31 | ]); 32 | }); 33 | 34 | 35 | $router->post('/titleize', function (ServerRequestInterface $request): ResponseInterface { 36 | $str = $request->getParsedBody()['data'] ?? ''; 37 | 38 | return new JsonResponse([ 39 | 'result' => s($str)->title(true)->toString() 40 | ]); 41 | }); 42 | 43 | $response = $router->dispatch($request); 44 | 45 | (new SapiEmitter())->emit($response); 46 | -------------------------------------------------------------------------------- /test/solutions/dependency-heaven/no-code/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-you-php/dependency-heaven", 3 | "description": "An introduction to Composer dependency management", 4 | "require": { 5 | "symfony/string": "^5.0 | ^6.0", 6 | "league/route": "^5.1", 7 | "laminas/laminas-diactoros": "^2.4", 8 | "laminas/laminas-httphandlerrunner": "^1.2 | ^2.4" 9 | } 10 | } -------------------------------------------------------------------------------- /test/solutions/dependency-heaven/no-code/solution.php: -------------------------------------------------------------------------------- 1 | post('/wat', function (ServerRequestInterface $request): ResponseInterface { 19 | $str = $request->getParsedBody()['data'] ?? ''; 20 | 21 | return new JsonResponse([ 22 | 'result' => s($str)->reverse()->toString() 23 | ]); 24 | }); 25 | 26 | $router->post('/snake', function (ServerRequestInterface $request): ResponseInterface { 27 | $str = $request->getParsedBody()['data'] ?? ''; 28 | 29 | return new JsonResponse([ 30 | 'result' => s($str)->snake()->toString() 31 | ]); 32 | }); 33 | 34 | 35 | $router->post('/titleize', function (ServerRequestInterface $request): ResponseInterface { 36 | $str = $request->getParsedBody()['data'] ?? ''; 37 | 38 | return new JsonResponse([ 39 | 'result' => s($str)->title(true)->toString() 40 | ]); 41 | }); 42 | 43 | $response = $router->dispatch($request); 44 | 45 | (new SapiEmitter())->emit($response); 46 | --------------------------------------------------------------------------------