diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0e36a0f..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,84 +0,0 @@ -# PHP CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-php/ for more details -# -version: 2.1 -jobs: - build: - docker: - - image: circleci/php:7.4-node-browsers - - steps: - - checkout - - - run: sudo apt update - - run: sudo apt install aspell - - run: sudo docker-php-ext-install zip - - # Download and cache dependencies - - restore_cache: - keys: - # "composer.lock" can be used if it is committed to the repo - - v1-dependencies-{{ checksum "composer.json" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: composer install -n --prefer-dist - - - save_cache: - key: v1-dependencies-{{ checksum "composer.json" }} - paths: - - ./vendor - - - run: ./test.sh - - run: ./vendor/bin/psalm.phar - - run: - name: Check Sample project - command: | - cd sample-project - composer install - ../vendor/bin/psalm.phar - - run: php generate.php - - - store_artifacts: - path: generated - - run: - name: Display link to built website - command: | - echo "Generated website available at:" - echo https://circleci.com/api/v1.1/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BUILD_NUM}/artifacts/0/generated/01-introduction.html - - persist_to_workspace: - root: ~/ - paths: project/generated - - deploy_ghpages: - docker: - - image: buildpack-deps:trusty - steps: - - checkout # just needed to get the SSH key for github so we can push later - - add_ssh_keys - - attach_workspace: - at: /tmp/workspace - - run: - name: Deploy to GitHub Pages - command: | - find /tmp/workspace - cd ~/project - git checkout gh-pages - mv .git/ /tmp/ - find . -mindepth 1 -delete - mv /tmp/.git . - cp -a /tmp/workspace/project/generated/. . - git add --no-ignore-removal . - git config user.email ci@circleci - git config user.name "Circle CI" - git commit -m "Update GH Pages based on CircleCI Build $CIRCLE_BUILD_NUM [ci skip]" - git config --global push.default simple - git push -workflows: - Workflow: - jobs: - - build - - deploy_ghpages: - requires: - - build diff --git a/.gitignore b/.gitignore deleted file mode 100644 index fca3abc..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/generated -/vendor diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/.spelling/wordlist b/.spelling/wordlist deleted file mode 100644 index 228f082..0000000 --- a/.spelling/wordlist +++ /dev/null @@ -1,96 +0,0 @@ -personal_ws-1.1 en 0 utf-8 -AModeratelyShortPhpTutorial -Analyzing -BDD -Banas -Behat -Brumbaugh's -CMS -CentOs -CircleCI -DSN -DocBlock -Drupal -EOL -Homebrew -Http -IDEs -JS -Jez -Laracasts -Laravel -Laurance -Magrathea -MariaDB -MySQL -NTS -OO -OPcache -Ondřej -PDO -PHP -PHP's -PHPDoc -PHPUnit -PSL -PSR -PhpStorm -PossiblyInvalidArrayAccess -Remi's -SQLite -SQLite -SUT -Symfony -Templating -XAMPP -Xampp -Zend -autoload -autoloader -br -ci -circleci -cli -color -config -const -ctrl -dev -devs -disprefer -dnf -docblock -elseif -endif -eval -getter -getters -goto -html -http -init -instanceof -insteadof -isset -iterable -json -localhost -md -natsort -nbsp -perl -phar -php -pre -psr -regexes -semver -setUp -sqlite -src -sudo -superglobals -templating -tmp -vimeo -xml \ No newline at end of file diff --git a/01-introduction.html b/01-introduction.html new file mode 100644 index 0000000..947a6ff --- /dev/null +++ b/01-introduction.html @@ -0,0 +1,118 @@ + + + + + + + + Introduction | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ +
+ +
+
+

Introduction

+

Who is this for?

+

This is for anyone who wants to learn to program in PHP.

+

You should probably already know at least one programming language with curly braces, if conditions, variables, functions and +loops. The most likely scenario is that you are a Javascript or Typescript developer, working on a team that uses +a combination of PHP and JS or TS, and while you might not currently need deep PHP knowledge, +you want 'T-shaped skills', involving basic PHP knowledge. There will be some comparisons with Javascript and +Typescript.

+

Why did I write it?

+

I work as a PHP developer, and have some colleagues with great Javascript skills who sometimes want to do work in the PHP +side of our application, but don't have PHP experience from their previous jobs. I looked around for an introductory +tutorial to recommend they can work through, but I didn't find anything that seemed exactly right.

+

Approach

+

This is a step-by-step guide to using a selection of the most important aspects of PHP. It doesn't attempt to be +comprehensive.

+

PHP is an old language that has evolved organically over the years. There are many ways to do things, +and many parts of the language that are best avoided in new code. This tutorial will concentrate on recommended +ways to use PHP, and ignore most of the things that PHP allows you to do but you probably shouldn't.

+
+ + + + + +
+ + + + +
+ + diff --git a/02-getting-php.html b/02-getting-php.html new file mode 100644 index 0000000..608f69a --- /dev/null +++ b/02-getting-php.html @@ -0,0 +1,129 @@ + + + + + + + + Getting PHP | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Getting PHP

+

If you just want to run a PHP one-liner, or experiment with a tiny throwaway PHP script, the easiest way is probably +online at 3v4l, but to work through this tutorial you should have PHP installed on your computer.

+

Installation

+

First check if you already have PHP installed. Open your command-line prompt, and type php -v. If you have PHP, you +should see something like:

+
PHP 7.4.2 (cli) (built: Jan 23 2020 11:21:30) ( NTS )
+Copyright (c) The PHP Group
+Zend Engine v3.4.0, Copyright (c) Zend Technologies
+    with Zend OPcache v7.4.2, Copyright (c), by Zend Technologies
+

Check the version number - ideally it should be 7.4.x but at minimum to follow this tutorial it needs to be at least +7.2.

+

Very few people download PHP directly from php.net. If you work with PHP developers you may want to check where +they install their PHP packages from and do the same. Installation methods vary with your operating system:

+

Linux

+

On Linux, use your package manager to install PHP, e.g. with the sudo apt install php command on Ubuntu or Debian, and +sudo dnf -y install php-cli on Fedora. However these may not give you a recent enough version of PHP. You can get +more up to date packages for Ubuntu from Ondřej Surý for Ubuntu or +from Remi's RPM Repository for Red Hat, CentOs and Fedora.

+

Macintosh

+

Apple supplies PHP as part of OS X, but this is 7.1, and you need at least 7.2. The best way to get that is probably +through Homebrew. If you don't have it, first install the Homebrew package manager by following the instructions at +brew.sh, and then enter the command brew install php && brew link php.

+

Windows

+

I personally don't have a Windows machine, but I think the easiest way to set up PHP on Windows may be as part of the +XAMPP package. Download and run the installer from Apache Friends. This +package bundles PHP along with the Apache web server, MariaDB database server, and the perl language. Xampp is also available +for Linux and Macintosh.

+

Run php -v again. Hopefully you will now have version 7.2 or later.

+
+ + + + + +
+ + + + +
+ + diff --git a/03-hello-world.html b/03-hello-world.html new file mode 100644 index 0000000..7a9eb63 --- /dev/null +++ b/03-hello-world.html @@ -0,0 +1,121 @@ + + + + + + + + Hello, world | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Hello, world

+

At this point we're ready to start writing in PHP. Make a new folder for your PHP code.

+

If you have a favourite text editor or integrated development environment, such as Vim, Atom, or VS Code you may want to +stick with that, but the best tool for editing PHP code is almost certainly JetBrains' +PhpStorm. PhpStorm is free for 30 days, but will demand money after that.

+

Make a new file called hello.php with your editor or IDE of choice, and type the following:

+
<?php declare(strict_types=1);
+
+echo "Hello, world.\n";
+

Save the file, and go to your command line. Navigate to your PHP code folder, and run it by +typing php hello.php. You should see:

+
$ php hello.php
+Hello, world
+$
+

Every PHP file should start with <?php declare(strict_types=1);. <?php tells the PHP interpreter that you're writing +PHP, and strict_types tells PHP not to try to guess what you mean so much. Historically PHP has tended to be very +loose, and prefer to make the best guess of what's required if it isn't clear instead of giving up. That's probably good +for writing a website one page at a time, but for applications we need the computer to stop and tell us what's wrong +if we make a mistake, not plough ahead and give us a wrong result.

+

echo simply outputs strings of text. We could use print, which is quicker to say, but we prefer echo, which is +20% shorter to write.

+
+ + + + + +
+ + + + +
+ + diff --git a/04-http.html b/04-http.html new file mode 100644 index 0000000..2e379c4 --- /dev/null +++ b/04-http.html @@ -0,0 +1,112 @@ + + + + + + + + HTTP | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

HTTP

+

PHP is of course mostly used in web servers, so you might be wondering why we started with a command line script.

+

In fact you can access the same script through your web browser. PHP comes with a built-in web server for development +and testing uses. To start it, run:

+
php -S localhost:8080
+

Point your browser at http://localhost:8080/hello.php, and you should see +"Hello, world.".

+

Go back to your command line and press ctrl-c to shut down the server.

+

This tutorial treats PHP as a general purpose programming language - we won't focus on the specifics of running in a web +server. In a big web application only a small part of the code may need to deal with getting data in and out through +HTTP, and usually this will be abstracted away to some extent by a framework like Symfony or Laravel, or a CMS like +Wordpress or Drupal.

+
+ + + + + +
+ + + + +
+ + diff --git a/05-variables.html b/05-variables.html new file mode 100644 index 0000000..1a0745b --- /dev/null +++ b/05-variables.html @@ -0,0 +1,175 @@ + + + + + + + + Variables, arrays and loops | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Variables, arrays and loops

+

Variables in PHP always have a dollar sign $ at the start of their names, and you declare a variable by assigning a +value to it. PHP interpolates double-quoted strings with variables. Edit hello.php to use a variable:

+
<?php declare(strict_types=1);
+
+$planet = "earth";
+
+echo "Hello, $planet.\n";
+
+

You can probably guess what this will do when you run it on your command line or serve it to your browser.

+

Arrays and loops

+

PHP has a very versatile built-in type called array. You're unlikely to find much PHP code that doesn't use arrays +extensively, but they are easy to overuse at the cost of other more expressive types. Despite the name, a PHP array +isn't really an array as you may know it from other languages. A PHP array is an ordered iterable map, with keys +and values. The keys may be strings or integers, and by default will be sequential integers starting at zero. The +values can be anything that could go in a variable, including more arrays.

+

An array is not an object in PHP. We will cover objects later.

+

Let's edit our script to declare an array and iterate through it:

+
<?php declare(strict_types=1);
+
+$planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'];
+
+foreach ($planets as $planet) {
+    echo "Hello, $planet.\n";
+}
+
+$dwarfPlanet = 'Pluto';
+echo "Hello, $dwarfPlanet.\n";
+

If you run this you should see:

+
$ php hello.php
+Hello, Mercury.
+Hello, Venus.
+Hello, Earth.
+Hello, Mars.
+Hello, Jupiter.
+Hello, Saturn.
+Hello, Uranus.
+Hello, Neptune.
+Hello, Pluto.
+
+

foreach assigns each value from the array to the $planet variable in turn. We edit the script to print keys as well +as values:

+
<?php declare(strict_types=1);
+
+$planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'];
+
+foreach ($planets as $number => $planet) {
+    echo "Hello, $planet, you are planet number $number.\n";
+}
+
+$dwarfPlanet = 'Pluto';
+echo "Hello, $dwarfPlanet.\n";
+

The PHP manual lists 81 functions for manipulating arrays. For now let's +try one: array_reverse. Add new line before foreach:

+
$planets = array_reverse($planets, true);
+

Re-run the script - the planets are now listed in reverse order. But notice that the array keys have not changed - +Mercury is still planet number 0, it's just that 0 now comes last. PHP array keys can come in any +order.

+

Associative arrays

+

We can also assign array keys explicitly. When we're interested in the keys of an array we call it an +associative array. For example:

+
<?php declare(strict_types=1);
+
+$planetPopulations = [
+    'Mercury' => 0,
+    'Venus' => 0,
+    'Earth' => 7.7 * 10**9,
+    'Mars' => 0,
+    'Jupiter' => 0,
+    'Saturn' => 0,
+    'Uranus' => 0,
+    'Neptune' => 0,
+];
+
+echo "The population of Earth is {$planetPopulations['Earth']}.\n";
+
+ + + + + +
+ + + + +
+ + diff --git a/06-functions.html b/06-functions.html new file mode 100644 index 0000000..f1ff618 --- /dev/null +++ b/06-functions.html @@ -0,0 +1,132 @@ + + + + + + + + Functions | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Functions

+

PHP comes with hundreds of built in functions, to sort arrays, find the +length of strings, match regexes, or even get the timestamp corresponding to midnight on Easter of a given +year, but you will inevitably want to define your own functions. +Function definitions look like this:

+
function getGreeting(string $planetName): string
+{
+    return "Hello, $planetName.\n";
+}
+

Notice the type declarations, string and : string. Strictly speaking these are optional, but you should add +them whenever you can. PHP is a gradually typed language, like TypeScript. However unlike Typescript, which does +type checking in the compiler, PHP applies type checking only when the relevant part of your program runs. Let's see +an example:

+
<?php declare(strict_types=1);
+
+function getGreeting(string $planetName): string
+{
+    return "Hello, $planetName.\n";
+}
+
+echo getGreeting('Magrathea');
+echo getGreeting(42);
+

Running this, you should see something like:

+
Hello, Magrathea.
+PHP Fatal error:  Uncaught TypeError: Argument 1 passed to getGreeting() must be of the type string, int given, called in functions.php on line 9 and defined in functions.php:3
+Stack trace:
+#0 functions.php(9): getGreeting()
+#1 {main}
+  thrown in functions.php on line 3
+

The first part of the program ran fine, but attempting to pass an integer to a function that we've declared with a +string parameter was a fatal error and caused a crash. If we'd tried to return anything other than a string from inside +the function that would also be a fatal error.

+
+ + + + + +
+ + + + +
+ + diff --git a/07-classes-1.html b/07-classes-1.html new file mode 100644 index 0000000..2711692 --- /dev/null +++ b/07-classes-1.html @@ -0,0 +1,199 @@ + + + + + + + + Classes and Objects | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Classes and Objects

+

PHP supports class-based object oriented programming, heavily influenced by Java and similar languages. This is +different to the prototype based OO in JavaScript.

+

In the sort of PHP code I write almost 100% of the code in a project is in classes - some classes are used to make +objects, and others may just be convenient wrappers around groups of functions.

+

In PHP every object is an instance of a class, so you have to have a class before you can have an object. Let's write +a class to let us make Planet objects. How to write classes changed a bit in PHP 7.4. Enter the +following in a file called Planet.php:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+final class Planet
+{
+    private string $name;
+
+    private float $populationSize;
+
+    public function __construct(string $name, float $populationSize)
+    {
+        $this->name = $name;
+        $this->populationSize = $populationSize;
+    }
+
+   public function getName(): string
+   {
+       return $this->name;
+   }
+
+   public function getPopulationSize(): float
+   {
+       return $this->populationSize;
+   }
+}
+

Older PHP Versions

+

name and populationSize are the properties of the class, and they have string and float types respectively. +Before 7.4 PHP didn't allow us to specify types for properties. We still want to know what types of values we intend to +put in the properties, so we use a DocBlock instead. If you don't have 7.4 yet, change the property declarations +to:

+
    /**
+     * @var string
+     */
+    private $name;
+
+    /**
+     * @var float
+     */
+    private $populationSize;
+

These DocBlocks are ignored by the PHP engine, but they are very useful for us, and many tools and IDEs will read them. +The code inside the docblock is written in the PHPDoc +language.

+

Running the code

+

Run php Planet.php. You should see no output - a class by itself doesn't do anything. We will write code to use this +class on the next page.

+

What's in the class

+

Let's read through the class from top to bottom.

+
    +
  • +

    The entire file is in a namespace. This is useful to help distinguish our code from other people's code, and +to distinguish between submodules when our program gets bigger. We will keep all our code in this namespace. The +namespace is effectively a prefix, so the full name of the class is \AModeratelyShortPhpTutorial\Planet.

    +
  • +
  • +

    Planet is a final class. This prevents any other classes being written as subclasses of Planet. Subclassing is +beyond the scope of this tutorial, but for now we can say that it adds significant complexity, and if we don't need it +we should probably avoid it. It's therefore a good practice to make classes final by default - we can always delete the +word final if we ever find we do need to make a subclasses of Planet.

    +
  • +
  • +

    Planet has name and populationSize properties. When we create Planet objects every object will have its own copy +of these properties. The properties have private visibility - that means that the properties can only be directly read +or written by code within the Planet class.

    +

    In the PHP 7.4 code they are typed properties, so PHP will +do a type check at run time whenever we assign values to the properties, and it will only allow us to assign strings of +text and and floating point numbers - attempting to assign anything else would cause a fatal error.

    +

    We would also get a fatal error if we tried to read these typed properties before assigning values to them.

    +
  • +
  • +

    Next we have three class functions, also known as methods. These all have public visibility, which means we can +call them from anywhere.

    +

    Functions whose names start with __ are considered Magic Methods in PHP - they have special meanings given by the +language. __construct is the Constructor, and will be automatically called whenever we create a Planet object with +the new keyword.

    +

    Finally we have our two getter functions. Since the properties are private, these public functions are needed to allow +code outside the class to read the properties. It's verbose, but it's a lot easier to understand what's happening in a big project +with a class like this than it would be if the properties were public and code from lots of other places could be +writing to them.

    +

    These functions will be run with a given object instance of the class. The $this variable will refer to that +object. We use the arrow -> operator to access the properties and methods of any object.

    +

    Having getters also means that if we later want to change the class - perhaps to replace the +$populationSize property with an array that holds details of every person on the planet - we can edit the code inside +the getter function and make it return the size of the array. Code that uses this class wouldn't have to be +affected by the change. Changes tend to ripple through a codebase, and one of the most important things to do when +choosing how to write code is anticipating types of future changes, and putting barriers in place to limit the spread of +those changes.

    +
  • +
+

If you know JavaScript, you can think of a class with public and private parts as serving a similar purpose to a +JavaScript module that exports some but not all of its symbols.

+
+ + + + + +
+ + + + +
+ + diff --git a/08-classes-2.html b/08-classes-2.html new file mode 100644 index 0000000..c4bb13a --- /dev/null +++ b/08-classes-2.html @@ -0,0 +1,189 @@ + + + + + + + + Classes and Composer | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Classes and Composer

+

On the last page we wrote Planet class, and saved it in a file called Planet.php. Now we want to put that class +to work.

+

PHP works by executing whatever script we give it from beginning to end. It won't run anything inside a class unless +we that script tells it to, so every PHP program needs at least one line that isn't part of any class as an entry-point. +There is no equivalent to the main function of languages like C and Java.

+

By convention, and to follow the PSR-1 coding standard, we always put the entry +point in a separate file to the class declaration.

+

Let's write a simple script to create a planet and print out its details. Since at the moment Planet is just a data +holder the script won't be too exciting.

+

Write the following in a file called start.php

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+$planet = new Planet('Neptune', 0);
+
+echo "Planet {$planet->getName()} has a population of {$planet->getPopulationSize()}.\n";
+

The new keyword creates objects instances from classes, and automatically calls any constructor the class has.

+

We can try running this now but it won't work just yet, because we need to link it up with Planet.php. When you type +php start.php you should see PHP Fatal error: Uncaught Error: Class 'Planet' not found.

+

Linking files together

+

There are two main ways to link files together. The old way is require_once, and the new ways is composer. We'll +start with require_once.

+

Require_once

+

Edit start.php to make it look like this:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__ . '/Planet.php';
+
+$planet = new Planet('Neptune', 0);
+
+echo "Planet {$planet->getName()} has a population of {$planet->getPopulationSize()}.\n";
+

Run the script again, and you should now see Planet Neptune has a population of 0.. The require_once statement +tells PHP to process the contents of the given file as if it had been pasted in to the current script, ignoring +the <?php opening tag and the declare. The once part means that if we require_once the same file more than once +PHP will skip it on the second and subsequent times. That's what want for a class - once the class is loaded there's no +need to load it again, even if multiple parts of our program have to declare that they need that class.

+

__DIR__ is a PHP magic constant that refers to the directory of whatever file its used in. The dot . is PHP's string +concatenation operator.

+

But adding a require_once statement every time we need to use a class can quickly become tedious. It's what we all +mostly did until around 2015, when the Composer dependency management tool became popular, even prompting a rare +mention for PHP in the ThoughtWorks Technology Radar. Nowadays we +almost always want to use Composer.

+

Composer

+

Composer is primarily a tool to help you add third party libraries and frameworks to your PHP projects, but since it +includes code to help us load the classes those libraries declare it makes sense to also use it to help us load our own +classes.

+

First check if you have composer installed - run composer about. On some systems it may be called composer.phar +instead of composer. If you have it, you should see "Composer - Dependency Manager for PHP". If not, +download and install composer from getcomposer.org, then come back to this page. I +suggest installing it to somewhere on your executable PATH, and naming it composer rather than composer.phar.

+

Let's adjust our script to use composer instead of require_once. First delete the require_once statement, to get back +to the class not found error we had before.

+

Create a subdirectory src and move Planet.php inside src. It's generally a good idea to have this to keep the bulk +of our source code separate from everything else in our project, e.g. the entry point file, any docs we might want to write, +tool config etc.

+

Composer works on a project-by-project basis. To set it up for your a project, you need to create a composer.json file. +To start with, just put an empty json object in this file:

+
{
+}
+

You can now run composer install. Composer should create a vendor subdirectory. Everything you install to you project +through composer will be in that directory. You would normally want to exclude vendor from source control.

+

Now we need to tell composer how to load your classes. The PSR-4 scheme is a standard way of translating between PHP +class names and file paths. Edit composer.json to look like the following:

+
{
+    "autoload": {
+        "psr-4": {
+            "AModeratelyShortPhpTutorial\\": "src/"
+        }
+    }
+}
+

Re-run composer install, and then edit start.php to require Composer's autoloader:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__. '/vendor/autoload.php';
+
+$planet = new Planet('Neptune', 0);
+echo "Planet {$planet->getName()} has a population of {$planet->getPopulationSize()}.\n";
+
+

As long as we keep our file names matching our class names, Composer will automatically load any class we need, and +only when we need it.

+

Of course technically we haven't got rid of the require_once statement, but by requiring the autoloader we won't +need to add any more require statements, however classes we use in our program.

+

If you run php start.php you should once again learn the population of Neptune.

+
+ + + + + +
+ + + + +
+ + diff --git a/09-classes-3.html b/09-classes-3.html new file mode 100644 index 0000000..a309aa9 --- /dev/null +++ b/09-classes-3.html @@ -0,0 +1,199 @@ + + + + + + + + Classes, Static Methods, and Object Identity | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Classes, Static Methods, and Object Identity

+

Static methods

+

Not all the methods on a class have to run in the context of an object. Methods that work without a $this object +instance are called static methods. Let's add a static method to the Planet class in src/Planet.php:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+final class Planet
+{
+    private string $name;
+
+    private float $populationSize;
+
+    public function __construct(string $name, float $populationSize)
+    {
+        $this->name = $name;
+        $this->populationSize = $populationSize;
+    }
+
+   public function getName(): string
+   {
+       return $this->name;
+   }
+
+   public function getPopulationSize(): float
+   {
+       return $this->populationSize;
+   }
+
+   public static function earth(): self
+   {
+       return new self('Earth', 7.7 * 10**9);
+   }
+}
+ +

The self keyword refers to whatever class it's written in. It's more convenient to write self than to repeat +Planet many times.

+

And let's edit start.php:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__. '/vendor/autoload.php';
+
+$planet = Planet::Earth();
+echo "Planet {$planet->getName()} has a population of {$planet->getPopulationSize()}.\n";
+

The double colon is the Scope Resolution Operator. It accesses static methods (and properties) of classes - there is +no need to have an object first. In this case our method returns an instance of the class, but it could do anything.

+

Object Identity

+

A PHP variable can't actually hold an object - instead it holds an object identifier, also known as a reference.

+

Two objects created the same way, with the same properties will have distinct identities. But if one object is created +and then assigned to two variables, they will both hold identifiers for the same object. This becomes important when we +make our objects mutable. This extra complexity is a good reason to prefer immutable objects, but sometimes we need +mutability.

+

Let's add a function to change the world - edit src/Planet.php again, adding the following function inside the class:

+
   public function receiveImmigrant(): void
+   {
+       $this->populationSize++;
+   }
+

void is the return type for functions that don't actually return any information.

+

Now let's write a script to illustrate object identities. Enter the following in 'identities.php'

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__. '/vendor/autoload.php';
+
+$mercury = new Planet('Mercury', 0);
+$secondMercury = new Planet('Mercury', 0);
+$theSameMercury = $mercury;
+
+echo "Mercury is equal to second Mercury:\n";
+var_dump($mercury == $secondMercury);
+echo \PHP_EOL;
+
+echo "But Mercury is not **identical** to second Mercury:\n";
+var_dump($mercury === $secondMercury);
+echo \PHP_EOL;
+
+echo "Mercury is equal **and** identical to the same Mercury:\n";
+var_dump($mercury == $theSameMercury);
+var_dump($mercury === $theSameMercury);
+echo \PHP_EOL;
+
+$mercury->receiveImmigrant();
+
+echo "Population of Mercury: {$mercury->getPopulationSize()}.\n";
+echo "Population of second Mercury: {$secondMercury->getPopulationSize()}.\n";
+echo "Population of the same Mercury: {$theSameMercury->getPopulationSize()}.\n";
+

We see that $mercury and $theSameMercury are just two names for the same object, while $secondMercury is an +entirely separate object with its own properties, lifecycle, hopes and dreams.

+

If a function accepts an object as a parameter, or returns an object, PHP doesn't make a copy of the object - it just +copies the identifier. This means that code within and without the function can access and potentially change the same +object. It's an important part of how communication happens between the parts of a PHP program, but it can easily get +confusing if not managed carefully.

+
+ + + + + +
+ + + + +
+ + diff --git a/10-testing.html b/10-testing.html new file mode 100644 index 0000000..6eafcc2 --- /dev/null +++ b/10-testing.html @@ -0,0 +1,206 @@ + + + + + + + + Testing | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Testing

+

If we want to work on even a moderately complex program over time, we need automated testing - manually testing +everything every time we make a change would quickly become unsustainable.

+

Installing PHPUnit

+

The leading test framework for PHP is Sebastian Bergmann's PHPUnit.

+

Since we already have composer set up for our project, we can use that to install PHPUnit in the vendor directory. Run:

+
composer require --dev phpunit/phpunit
+

Composer also automatically downloads and installs all the libraries that PHPUnit depends on, and the dependencies of +its dependencies, etc.

+

We use the --dev option because PHPUnit is a tool for developers, and not a library that our program would rely on +in production. If we wanted to prepare a copy of our program to install on a server, we would use +composer install --no-dev install any libraries we need and set up the autoloader but leave out PHPUnit.

+

When we ran the require command composer.json edited our composer.json file to record our updated requirements. +composer.json should now look like:

+
{
+    "autoload": {
+        "psr-4": {
+            "AModeratelyShortPhpTutorial\\": "src/"
+        }
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.0"
+    }
+}
+

The last major release of PHPUnit was 9.0, so composer has assumed we will always want whatever the latest PHPUnit +release in the 9 series is. The 10 series is not expected to be compatible with code written for PHPUnit 9, so composer +won't install that unless we edit composer json. Composer works best with dependencies that use semantic versioning.

+

Composer has also created a new file for us, composer.lock. This has metadata about the exact versions of the packages +installed. At the time of writing it shows me that PHPUnit is at version 9.0.1, and I can see the details of 29 other +packages that have been installed because PHPUnit depends on them directly or indirectly. The composer show command +will output the list of installed packages in a much more concise format.

+

Writing a test

+

Let's write our first test. Create a test subdirectory next to src, and write the following in test/PlanetTest.php

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+use PHPUnit\Framework\TestCase;
+
+final class PlanetTest extends TestCase
+{
+    private Planet $SUT; // SUT = Subject Under Test
+
+    public function setUp(): void
+    {
+        $this->SUT = new Planet('planet name', 0);
+    }
+
+    public function test_it_can_accept_immigrant(): void
+    {
+        $this->assertSame(0.0, $this->SUT->getPopulationSize());
+        $this->SUT->receiveImmigrant();
+        $this->assertSame(1.0, $this->SUT->getPopulationSize());
+    }
+}
+

The first new keyword here is use. This is syntactic sugar that saves us having to spell out the name of the class +PHPUnit\Framework\TestCase in full when we use below.

+

We also meet the extends keyword here. This means that our class is an extension, or subclass, of PHPUnit's TestCase +class, which is how PHPUnit is designed to be used. If TestCase has been marked final we wouldn't be able to extend +it.

+

If you don't have PHP 7.4, remember to remove the Planet property type of the SUT, and replace it with a docbloc as we +did for the properties of Planet itself.

+

It's prudent to see a test fail at least once before believing what it says when it passes. To make it fail, comment out +$this->populationSize++; in src/Planet.php:

+

// $this->populationSize++;

+

Now run the PHPUnit command:

+
vendor/bin/phpunit test
+

This will search for any filenames ending in Test.php in the test directory. In each test case any public function +whose name starts with test is considered a test. For every test PHPUnit creates an instance of the class, calls the +setup function, then calls the test function, records the results, and then throws away the object. So if we had two +tests it would call setUp twice. Any object referenced only by garbage is garbage, so when the test case +object is thrown away the Planet is thrown away too, and any mutations to the planet will not affect the next test.

+

The output from PHPUnit should look like:

+
PHPUnit 9.0.1 by Sebastian Bergmann and contributors.
+
+F                                                                   1 / 1 (100%)
+
+Time: 36 ms, Memory: 4.00 MB
+
+There was 1 failure:
+
+1) AModeratelyShortPhpTutorial\PlanetTest::test_it_can_accept_immigrant
+Failed asserting that 0.0 is identical to 1.0.
+
+/tmp/composerPlayground/test/PlanetTest.php:20
+
+FAILURES!
+Tests: 1, Assertions: 2, Failures: 1.
+

Fix the Planet class putting the increment back in, and re-run the PHPUnit command. We should now see some happier +output:

+
PHPUnit 9.0.1 by Sebastian Bergmann and contributors.
+
+.                                                                   1 / 1 (100%)
+
+Time: 35 ms, Memory: 4.00 MB
+
+OK (1 test, 2 assertions)
+

Writing tests is a huge topic, which we can't cover in detail here. PHPUnit has excellent +official documentation. You might want to do +Test Driven Development (TDD) and/or Behaviour Driven Development (BDD) and write your tests before writing the +production code that they cover.

+

Some other major test frameworks for PHP are PHPSpec and +Behat. These are both designed around the BDD approach, which +uses the language of executable specifications rather than tests. A major difference between them is that in PHPSpec, +as with PHPUnit, you code in PHP. In Behat you code in a separate language called gherkin, designed to look like English +and be readable by people who haven't been trained in programming.

+
+ + + + + +
+ + + + +
+ + diff --git a/11-databases.html b/11-databases.html new file mode 100644 index 0000000..c546fa1 --- /dev/null +++ b/11-databases.html @@ -0,0 +1,277 @@ + + + + + + + + Databases | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Databases

+

PHP applications often connect to databases. This is even harder to avoid in PHP than it would be in other languages +you can run on a web server like JS, Java, or C#, because PHP has a shared-nothing architecture. That means that the +code dealing with each request runs in isolation, and does not share any objects or variables with other requests. At +the end of the request-response cycle all stack frames are unwound and all objects become garbage.

+

So if your program needs to persist anything from one page to the next, a database is a natural choice.

+

For simplicity, we will use SQLite. This is not a database server that you connect to, +but a full featured SQL database engine supplied as a C library, and available as an add-on module for PHP, and the +world's most widely deployed DB engine. An SQLite database is just a file.

+

The details of SQL, the programming language for defining and querying databases, are beyond +the scope of this tutorial. We will see some SQL code, but as far as PHP is concerned they are just strings to send to +the database engine.

+

Install the SQLite PHP Module

+

We will use our SQLite database via PHP's PHP Data Objects (PDO) extension, which provides a consistent interface for +accessing many different DB types.

+

First check if the necessary PHP modules are installed. Run php -i | grep sqlite. If you have the module set up, +you should see a line like "PDO drivers => sqlite". There may be other drivers listed alongside sqlite.

+

If you don't have the PDO SQLite module, how to install it will depend on your OS and how you installed PHP.

+

Linux

+

If you installed PHP through your package manager, there should be an SQLite module available in the same place. Make +sure you get the module that matches your PHP version number. For instance, on Debian or Ubuntu search your repository +with:

+

apt-cache search php | grep sqlite

+

Once you've found the package name, e.g. php7.4-sqlite3 install it with a command such as

+

sudo apt install php7.4-sqlite3.

+

Mac

+

If you installed PHP via homebrew, I think it will automatically have come with SQLite built in and enabled.

+

Windows

+

If you installed PHP as part of XAMPP this should have SQLite enabled by default.

+

Creating a database

+

First, let's write a PHP script src/create-database.php to create a new database with one table:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+use PDO;
+
+$filename =__DIR__ . "/database.sqlite";
+
+file_put_contents($filename, '');
+
+$connection = new PDO("sqlite:" . $filename);
+
+$connection->query('
+    CREATE TABLE planets (
+        id INTEGER PRIMARY KEY,
+        name text,
+        population_size INTEGER
+    );
+'
+);
+

Run the script with php create-database.php. It should create a new binary file database.sqlite.

+

file_put_contents writes a string to a file, overwriting the file if it already exists. We're using here to create +a completely empty file.

+

The PDO class is supplied by the PDO extension. A PDO object represents +a connection to a database, and allows us to send it queries and get back results. When we call the +PDO constructor we pass it a string holding details of the database +we want to connect to, known as a Data Source Name or DSN. For SQLite the DSN consists of 'sqlite:' followed by an +absolute file path.

+

Querying the database

+

Let's write a very small program to put the planets in the database, and let us view info about any planet. First we +will need a class that can insert all the planets. Make a file src/PlanetStore.php

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+use PDO;
+
+final class PlanetStore
+{
+    private PDO $connection;
+
+    public function __construct(PDO $connection) {
+        $this->connection = $connection;
+    }
+
+    public function storePlanets(): void
+    {
+        $statement = $this->connection->prepare('INSERT INTO planets (name, population_size) values (?, ?);');
+        foreach ($this->generatePlanets() as $planet) {
+            $statement->execute([$planet->getName(), $planet->getPopulationSize()]);        
+        }
+    }
+
+    /** @return Planet[] */
+    private function generatePlanets(): array
+    {
+        return [
+            new Planet('Mercury', 0),
+            new Planet('Venus', 0),
+            new Planet('Earth', 7.7 * 10**9),
+            new Planet('Mars', 0),
+            new Planet('Jupiter', 0),
+            new Planet('Saturn', 0),
+            new Planet('Uranus', 0),
+            new Planet('Neptune', 0),
+        ];
+    } 
+}
+

Once again if you don't have PHP 7.4 you will need to remove the PDO property type on the $connection property.

+

Now write a script storePlanets.php to use this class:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__ . '/vendor/autoload.php';
+
+use PDO;
+
+$planetStore = new PlanetStore(
+    new PDO("sqlite:". __DIR__ . '/database.sqlite')
+);
+
+$planetStore->storePlanets();
+

Run php storePlanets.php. This should add the eight planets to the database. It shouldn't produce any visible output.

+

The most important thing to notice about our code so far is that we didn't concatenate any strings to create queries to +send to the database. Instead we prepared a query, using question marks as placeholders for our data, and then supplied +the values of those parameters separately each time we executed the query.

+

This practice makes our database use much less error prone and more secure. The database engine knows what exactly +what's code that it should interpret and what's data that it should simply store.

+

Now let's write the code to show a planet. First let's add a function to the PlanetStore class to pull a Planet out of +the database:

+
    public function getPlanet(string $planetName): ?Planet
+    {
+        $statement = $this->connection->prepare(
+            'SELECT name, population_size FROM planets where name = :name;'
+        );
+        $statement->bindValue(':name', $planetName);
+        $statement->execute();
+
+        $row = $statement->fetch(PDO::FETCH_ASSOC);
+
+        if (! $row) {
+            return null;
+        }
+
+        return new Planet($row['name'], (float) $row['population_size']);
+    }
+

Notice the return type: ?Planet means that the function may either return an instance of Planet, or the value null. +We can add ? to the beginning of any type name to make it nullable.

+

We use (float) to cast or convert the population size data from a string, as it comes back from the sqlite DB, to +a floating point number.

+

In this case we have used a named parameter in the database query, instead of a question mark.

+

Finally we need to make a PHP script to go between this function and the browser. Call it viewPlanet.php:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__ . '/vendor/autoload.php';
+
+use PDO;
+
+$planetStore = new PlanetStore(
+    new PDO("sqlite:". __DIR__ . '/database.sqlite')
+);
+
+$planet = $planetStore->getPlanet((string)$_GET['name']);
+
+header('Content-Type: text/plain');
+if ($planet === null) {
+    http_response_code(404);
+    echo ('Planet not found');
+    exit();
+}
+
+echo "**********   {$planet->getName()}   **********\n\n";
+echo "Population: {$planet->getPopulationSize()}\n";
+

Notice the $_GET array - PHP automatically fills this so-called superglobal array with any query parameters sent by +the browser. It is available to us anywhere in our program, but it's best to limit where we use it - code that directly +pulls data from superglobals can be very hard to test and re-use and understand.

+

Run the PHP server:

+
php -S localhost:8080
+

Open http://localhost:8080/viewPlanet.php?name=Earth in your browser.

+

Other Database Engines.

+

PDO has drivers for eleven other database engines that you can use +use instead of SQLite. Connecting, sending queries, and receiving rows should work in the same way, but of course the +details of connection strings and SQL code will vary from DB to DB.

+
+ + + + + +
+ + + + +
+ + diff --git a/12-templating.html b/12-templating.html new file mode 100644 index 0000000..d489c51 --- /dev/null +++ b/12-templating.html @@ -0,0 +1,169 @@ + + + + + + + + Templating | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Templating

+

Our planet viewer works, but it isn't pretty. Let's use some HTML to make it look a little better.

+

It has been a common practice to use PHP as a templating language, and even mix PHP and HTML code together in the same +file. While PHP can still be used as a templating language, it isn't a good one, so we won't do that.

+

Instead, we will use PHP to prepare whatever data we want to show, and then pass that to a template written in a +different language.

+

There are dozens of template engines available as libraries to use in PHP programs, and another good option +is to make a single page application, with all templating done in the browser, and data sent from the server as JSON.

+

Twig

+

For this tutorial we will write a template in the Twig language, which works with PHP.

+

First install Twig in your project by running:

+
composer require "twig/twig:^3.0"
+

Now let's make our template. Start by writing a page in HTML. For now we'll hard code everything. Make a +'templates' directory, and save the following as 'templates/planet.html.twig':

+
<!doctype html>
+<html>
+    <head>
+        <title>Some Planet</title>
+    </head>
+    <body>
+        <h1>Some Planet</h1>
+        <p>Population: 34601</p>
+    </body>
+</html>
+

Edit our viewPlanet.php file to make it use this template:

+
<?php declare(strict_types=1);
+
+namespace AModeratelyShortPhpTutorial;
+
+require_once __DIR__ . '/vendor/autoload.php';
+
+use PDO;
+use Twig\Loader\FilesystemLoader;
+
+$planetStore = new PlanetStore(
+    new PDO("sqlite:". __DIR__ . '/database.sqlite')
+);
+
+$templateLoader = new FilesystemLoader(__DIR__ . '/templates/');
+$twig = new \Twig\Environment($templateLoader);
+
+$planet = $planetStore->getPlanet((string)$_GET['name']);
+
+header('Content-Type: text/html');
+if ($planet === null) {
+    http_response_code(404);
+    echo ('Planet not found');
+    exit();
+}
+
+$template = $twig->load('planet.html.twig');
+
+echo $template->render();
+

If you run the PHP server again and view your site you should see the content of the template. Of course at this point +all we're doing is serving static HTML with extra steps.

+

Behind the scenes the Twig engine compiles the template to a PHP class. We don't need to read the class, but this is +useful because it means we can pass PHP objects to it and use their properties and functions. Let's pass our planet +object to the template. Change the last line of 'viewPlanet.php' to:

+
echo $template->render(['planet' => $planet]);
+

Finally we need to edit 'planet.html.twig'. Since we won't always be reading the template at the same time as the PHP file +that uses it, and neither will any tools and IDEs we might be using, we should add a comment to make it clear that we +expect to have a planet variable, and it will be of type Planet. Add the following to the top of the file:

+
+

In Twig we use double curly brackets to output dynamic data: Replace Some Planet with +{{planet.name}}, and replace 34601 with {{planet.populationSize}}. Twig knows that we mean getName and +getPopulationSize, so we don't have to write those function names out in full.

+

If you reload the page you should now see the planet details in their proper places.

+

Twig is a full featured special purpose programming language, with features like loops, conditionals, filters, +inheritance, etc, but this is a PHP tutorial, not a Twig tutorial.

+
+ + + + + +
+ + + + +
+ + diff --git a/13-static-analysis.html b/13-static-analysis.html new file mode 100644 index 0000000..d0654ca --- /dev/null +++ b/13-static-analysis.html @@ -0,0 +1,167 @@ + + + + + + + + Static Analysis | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Static Analysis

+

I recommend using at least one static analysis tool on any PHP project. While languages like TypeScript and Java have +explicit compilation phases that can quickly catch lots of errors, PHP doesn't. Most of the type checks in PHP happen only +as each line of code is executed.

+

Unit tests help, but 100% test coverage is unlikely, and even then we can only ever test with a few example inputs. For +more confidence we need static analysis as well, especially if we want to be able to easily refactor our code and upgrade +the libraries we're using.

+

Static Analysers for PHP include Psalm, PHPStan and +Phan. In this tutorial we will use Psalm.

+

Running Psalm

+

Install Psalm with Composer:

+
composer require --dev vimeo/psalm
+

Create a Psalm Config file:

+
./vendor/bin/psalm --init
+

Finally, run Psalm to check your code:

+
./vendor/bin/psalm
+

You should see something like:

+
Calculating best config level based on project files
+Calculating best config level based on project files
+Scanning files...
+Analyzing files...
+
+░E
+
+Detected level 7 as a suitable initial default
+Config file created successfully. Please re-run psalm.
+

Re-run Psalm as instructed. If your code is the same as mine, you should see "No errors found!", and +"1 other issues found. You can display them with --show-info=true".

+

That's OK, but where's the fun in a static analysis tool that doesn't complain about anything? Let's change the Psalm +settings to make it a lot stricter. Open 'psalm.xml' and change errorLevel="7" to errorLevel="1".

+

Re-run Psalm. Now you should see an error:

+

+ERROR: InvalidArgument - src/PlanetStore.php:52:27 - Argument 1 of AModeratelyShortPhpTutorial\Planet::__construct expects string, scalar provided
+        return new Planet($row['name'], (float)$row['population_size']); +

+

Psalm has looked at our code, and the code of the libraries and modules we're using, and found a mismatch. The planet +class constructor needs a string, but we've passed it something from the array returned by the +PDOStatement::fetch.

+

Psalm knows that when we call fetch with \PDO::FETCH_ASSOC we will either get false or an array of scalar +values (i.e. not objects). It can see that the value isn't false at line 52, because when it is false the function +returns early, skipping that line.

+

In this case we know our database a bit better than Psalm does, and we need to add a comment to tell Psalm that fetch +will return either false or an array of strings. Edit 'PlanetStore.php' and add docblock for the $row variable:

+
        /** @var array<string, string>|false $row */
+        $row = $statement->fetch(PDO::FETCH_ASSOC);
+

This docbloc means that the $row variable's type is the union of false and string-indexed, string-valued array. In +other words it either holds the value false, or it holds an array, in which any keys are strings and any values are +also strings. | combines types to make a union type. The < > brackets are used to provide arguments for a +generic type. false is an example of a literal type.

+

Psalm's type system extends the PHP type system, and is a lot more expressive. PHP itself does not have union, generic, +or literal types, although unions are coming in PHP 8. We can use types that PHP doesn't support by writing them in +docblocks, with tags such as +@var, +@param, and +@return, +and making sure we run our static analysis tool every time +we edit our source code.

+

Let's add a deliberate mistake to our code to see a bit more of what Psalm can help us with. Let's suppose we forgot +that the database might not have the planet we're looking for. Comment out the check for that in 'planetStore.php':

+
//        if (! $row) {
+//            return null;
+//        }
+

Re-run Psalm. It now reports two PossiblyInvalidArrayAccess errors, telling us that we +'Cannot access array value on non-array variable $row of type false'. This is a reminder that we should have made sure +that $row is not false before trying to use it as an array. If we leave it like this our server will produce an HTTP +500 internal server error instead of displaying the 404 page when someone asks to see the planet Pluto. Put the check +back to make Psalm happy again.

+
+ + + + + +
+ + + + +
+ + diff --git a/14-what-next.html b/14-what-next.html new file mode 100644 index 0000000..3f2a216 --- /dev/null +++ b/14-what-next.html @@ -0,0 +1,171 @@ + + + + + + + + What next? | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

What next?

+

This has only been a cursory introduction to the PHP language and ecosystem. Here are some ideas for things to try next, +in no particular order:

+ +
+ + + + + +
+ + + + +
+ + diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..4108d97 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +a-moderately-short-php-tutorial.com diff --git a/composer.json b/composer.json deleted file mode 100644 index e8de0be..0000000 --- a/composer.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "require": { - "erusev/parsedown": "^1.7", - "league/flysystem": "^1.0", - "twig/twig": "^3.0", - "psalm/phar": "^3.9" - } -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index a7cd283..0000000 --- a/composer.lock +++ /dev/null @@ -1,487 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "eed927d71bd15d303438830c6f97f342", - "packages": [ - { - "name": "erusev/parsedown", - "version": "1.7.4", - "source": { - "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", - "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35" - }, - "type": "library", - "autoload": { - "psr-0": { - "Parsedown": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Emanuil Rusev", - "email": "hello@erusev.com", - "homepage": "http://erusev.com" - } - ], - "description": "Parser for Markdown.", - "homepage": "http://parsedown.org", - "keywords": [ - "markdown", - "parser" - ], - "time": "2019-12-30T22:54:17+00:00" - }, - { - "name": "league/flysystem", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f3ad69181b8afed2c9edf7be5a2918144ff4ea32", - "reference": "f3ad69181b8afed2c9edf7be5a2918144ff4ea32", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "league/mime-type-detection": "^1.3", - "php": "^7.2.5 || ^8.0" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "phpspec/prophecy": "^1.11.1", - "phpunit/phpunit": "^8.5.8" - }, - "suggest": { - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "support": { - "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.1.4" - }, - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], - "time": "2021-06-23T21:56:05+00:00" - }, - { - "name": "league/mime-type-detection", - "version": "1.7.0", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", - "reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.18", - "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\MimeTypeDetection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frankdejonge.nl" - } - ], - "description": "Mime-type detection for Flysystem", - "support": { - "issues": "https://github.com/thephpleague/mime-type-detection/issues", - "source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0" - }, - "funding": [ - { - "url": "https://github.com/frankdejonge", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/flysystem", - "type": "tidelift" - } - ], - "time": "2021-01-18T20:58:21+00:00" - }, - { - "name": "psalm/phar", - "version": "3.9.3", - "source": { - "type": "git", - "url": "https://github.com/psalm/phar.git", - "reference": "8c01ea7f8c6eba045a9b977ee05cb0bfea1550eb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/psalm/phar/zipball/8c01ea7f8c6eba045a9b977ee05cb0bfea1550eb", - "reference": "8c01ea7f8c6eba045a9b977ee05cb0bfea1550eb", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "conflict": { - "vimeo/psalm": "*" - }, - "bin": [ - "psalm.phar" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Composer-based Psalm Phar", - "time": "2020-02-19T01:58:24+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" - }, - { - "name": "twig/twig", - "version": "v3.4.3", - "source": { - "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" - }, - "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Twig\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Twig Team", - "role": "Contributors" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "https://twig.symfony.com", - "keywords": [ - "templating" - ], - "support": { - "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.3" - }, - "funding": [ - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/twig/twig", - "type": "tidelift" - } - ], - "time": "2022-09-28T08:42:51+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" -} diff --git a/content/01-introduction.md b/content/01-introduction.md deleted file mode 100644 index b739026..0000000 --- a/content/01-introduction.md +++ /dev/null @@ -1,30 +0,0 @@ -{% block title%}Introduction{% endblock %} - -{% block body%} - -### Who is this for? - -This is for anyone who wants to learn to program in PHP. - -You should probably already know at least one programming language with curly braces, if conditions, variables, functions and -loops. The most likely scenario is that you are a Javascript or Typescript developer, working on a team that uses -a combination of PHP and JS or TS, and while you might not currently need deep PHP knowledge, -you want 'T-shaped skills', involving basic PHP knowledge. There will be some comparisons with Javascript and -Typescript. - -### Why did I write it? - -I work as a PHP developer, and have some colleagues with great Javascript skills who sometimes want to do work in the PHP -side of our application, but don't have PHP experience from their previous jobs. I looked around for an introductory -tutorial to recommend they can work through, but I didn't find anything that seemed exactly right. - -### Approach - -This is a step-by-step guide to using a selection of the most important aspects of PHP. It doesn't attempt to be -comprehensive. - -PHP is an old language that has evolved organically over the years. There are many ways to do things, -and many parts of the language that are best avoided in new code. This tutorial will concentrate on recommended -ways to use PHP, and ignore most of the things that PHP allows you to do but you probably shouldn't. - -{% endblock %} diff --git a/content/02-getting-php.md b/content/02-getting-php.md deleted file mode 100644 index ad8ddfa..0000000 --- a/content/02-getting-php.md +++ /dev/null @@ -1,47 +0,0 @@ -{% block title%}Getting PHP{% endblock %} - -{% block body%} - -If you just want to run a PHP one-liner, or experiment with a tiny throwaway PHP script, the easiest way is probably -online at [3v4l](https://3v4l.org/), but to work through this tutorial you should have PHP installed on your computer. - -### Installation - -First check if you already have PHP installed. Open your command-line prompt, and type `php -v`. If you have PHP, you -should see something like: - -``` -PHP 7.4.2 (cli) (built: Jan 23 2020 11:21:30) ( NTS ) -Copyright (c) The PHP Group -Zend Engine v3.4.0, Copyright (c) Zend Technologies - with Zend OPcache v7.4.2, Copyright (c), by Zend Technologies -``` - -Check the version number - ideally it should be **7.4.x** but at minimum to follow this tutorial it needs to be at least -**7.2**. - -Very few people download PHP directly from php.net. If you work with PHP developers you may want to check where -they install their PHP packages from and do the same. Installation methods vary with your operating system: - -### Linux - -On Linux, use your package manager to install PHP, e.g. with the `sudo apt install php` command on Ubuntu or Debian, and -`sudo dnf -y install php-cli` on Fedora. However these may not give you a recent enough version of PHP. You can get -more up to date packages for Ubuntu from [Ondřej Surý](https://launchpad.net/~ondrej/+archive/ubuntu/php) for Ubuntu or -from [Remi's RPM Repository](https://rpms.remirepo.net/wizard/) for Red Hat, CentOs and Fedora. - -### Macintosh - -Apple supplies PHP as part of OS X, but this is 7.1, and you need at least 7.2. The best way to get that is probably -through **Homebrew**. If you don't have it, first install the Homebrew package manager by following the instructions at -[brew.sh](https://brew.sh/), and then enter the command `brew install php && brew link php`. - -### Windows - -I personally don't have a Windows machine, but I think the easiest way to set up PHP on Windows may be as part of the -**XAMPP** package. Download and run [the installer from Apache Friends](https://www.apachefriends.org/index.html). This -package bundles PHP along with the Apache web server, MariaDB database server, and the perl language. Xampp is also available -for Linux and Macintosh. - -Run `php -v` again. Hopefully you will now have version **7.2 or later**. -{% endblock %} diff --git a/content/03-hello-world.md b/content/03-hello-world.md deleted file mode 100644 index bf67bea..0000000 --- a/content/03-hello-world.md +++ /dev/null @@ -1,34 +0,0 @@ -{% block title%}Hello, world{% endblock %} - -{% block body%} -At this point we're ready to start writing in PHP. Make a new folder for your PHP code. - -If you have a favourite text editor or integrated development environment, such as Vim, Atom, or VS Code you may want to -stick with that, but the best tool for editing PHP code is almost certainly JetBrains' -[PhpStorm](https://www.jetbrains.com/phpstorm/). PhpStorm is free for 30 days, but will demand money after that. - -Make a new file called `hello.php` with your editor or IDE of choice, and type the following: -```php - $planet) { - echo "Hello, $planet, you are planet number $number.\n"; -} - -$dwarfPlanet = 'Pluto'; -echo "Hello, $dwarfPlanet.\n"; -``` - -The PHP manual lists [81 functions for manipulating arrays](https://www.php.net/manual/en/ref.array.php). For now let's -try one: [array_reverse](https://www.php.net/manual/en/function.array_reverse.php). Add new line before `foreach`: - -```php -$planets = array_reverse($planets, true); -``` - -Re-run the script - the planets are now listed in reverse order. But notice that the array keys have not changed - -Mercury is still planet number `0`, it's just that `0` now comes last. PHP array keys can come in any -order. - -### Associative arrays - -We can also assign array keys explicitly. When we're interested in the keys of an array we call it an -*associative array*. For example: - -```php - 0, - 'Venus' => 0, - 'Earth' => 7.7 * 10**9, - 'Mars' => 0, - 'Jupiter' => 0, - 'Saturn' => 0, - 'Uranus' => 0, - 'Neptune' => 0, -]; - -echo "The population of Earth is {$planetPopulations['Earth']}.\n"; -``` -{% endblock %} diff --git a/content/06-functions.md b/content/06-functions.md deleted file mode 100644 index b01cc95..0000000 --- a/content/06-functions.md +++ /dev/null @@ -1,46 +0,0 @@ -{% block title%}Functions{% endblock %} - -{% block body%} - -PHP comes with [hundreds](https://www.php.net/manual/en/funcref.php) of built in functions, to sort arrays, find the -length of strings, match regexes, or even [get the timestamp corresponding to midnight on Easter of a given -year](https://www.php.net/manual/en/function.easter-date.php), but you will inevitably want to define your own functions. -Function definitions look like this: - -```php -function getGreeting(string $planetName): string -{ - return "Hello, $planetName.\n"; -} -``` - -Notice the **type declarations**, `string` and `: string`. Strictly speaking these are optional, but you should add -them whenever you can. PHP is a *gradually typed* language, like TypeScript. However unlike Typescript, which does -type checking in the compiler, PHP applies type checking only when the relevant part of your program runs. Let's see -an example: - -```php -name = $name; - $this->populationSize = $populationSize; - } - - public function getName(): string - { - return $this->name; - } - - public function getPopulationSize(): float - { - return $this->populationSize; - } -} -``` - -### Older PHP Versions - -`name` and `populationSize` are the properties of the class, and they have `string` and `float` types respectively. -Before 7.4 PHP didn't allow us to specify types for properties. We still want to know what types of values we intend to -put in the properties, so we use a **DocBlock** instead. If you don't have 7.4 yet, change the property declarations -to: - -``` - /** - * @var string - */ - private $name; - - /** - * @var float - */ - private $populationSize; -``` - -These DocBlocks are ignored by the PHP engine, but they are very useful for us, and many tools and IDEs will read them. -The code inside the docblock is written in the [PHPDoc](https://docs.phpdoc.org/latest/references/phpdoc/index.html) -language. - -### Running the code - -Run `php Planet.php`. You should see no output - a class by itself doesn't do anything. We will write code to use this -class on the next page. - -### What's in the class - -Let's read through the class from top to bottom. - -* The entire file is in a **namespace**. This is useful to help distinguish our code from other people's code, and -to distinguish between submodules when our program gets bigger. We will keep all our code in this namespace. The -namespace is effectively a prefix, so the full name of the class is ``\AModeratelyShortPhpTutorial\Planet``. - -* Planet is a **final** class. This prevents any other classes being written as *subclasses* of Planet. Subclassing is -beyond the scope of this tutorial, but for now we can say that it adds significant complexity, and if we don't need it -we should probably avoid it. It's therefore a good practice to make classes final by default - we can always delete the -word final if we ever find we do need to make a subclasses of Planet. - -* Planet has `name` and `populationSize` **properties**. When we create Planet objects every object will have its own copy -of these properties. The properties have `private` **visibility** - that means that the properties can only be directly read -or written by code within the Planet class. - - In the PHP 7.4 code they are **typed properties**, so PHP will -do a type check at run time whenever we assign values to the properties, and it will only allow us to assign strings of -text and and floating point numbers - attempting to assign anything else would cause a fatal error. - - We would also get a fatal error if we tried to read these typed properties before assigning values to them. - -* Next we have three class **functions**, also known as methods. These all have `public` visibility, which means we can -call them from anywhere. - - Functions whose names start with `__` are considered **Magic Methods** in PHP - they have special meanings given by the -language. `__construct` is the **Constructor**, and will be automatically called whenever we create a Planet object with -the `new` keyword. - - Finally we have our two *getter* functions. Since the properties are private, these public functions are needed to allow -code outside the class to read the properties. It's verbose, but it's a lot easier to understand what's happening in a big project -with a class like this than it would be if the properties were public and code from lots of other places could be -writing to them. - - These functions will be run with a given object instance of the class. The `$this` variable will refer to that - object. We use the arrow `->` operator to access the properties and methods of any object. - - Having getters also means that if we later want to change the class - perhaps to replace the -`$populationSize` property with an array that holds details of every person on the planet - we can edit the code inside -the getter function and make it return the size of the array. Code that uses this class wouldn't have to be -affected by the change. Changes tend to ripple through a codebase, and one of the most important things to do when -choosing how to write code is anticipating types of future changes, and putting barriers in place to limit the spread of -those changes. - -If you know JavaScript, you can think of a class with public and private parts as serving a similar purpose to a -JavaScript module that exports some but not all of its symbols. -{% endblock %} \ No newline at end of file diff --git a/content/08-classes-2.md b/content/08-classes-2.md deleted file mode 100644 index c948c32..0000000 --- a/content/08-classes-2.md +++ /dev/null @@ -1,133 +0,0 @@ -{% block title%}Classes and Composer{% endblock %} - -{% block body%} - -On the last page we wrote **Planet** class, and saved it in a file called **Planet.php**. Now we want to put that class -to work. - -PHP works by executing whatever script we give it from beginning to end. It won't run anything inside a class unless -we that script tells it to, so every PHP program needs at least one line that isn't part of any class as an entry-point. -There is no equivalent to the `main` function of languages like C and Java. - -By convention, and to follow the [PSR-1](https://www.php-fig.org/psr/psr-1/) coding standard, we always put the entry -point in a separate file to the class declaration. - -Let's write a simple script to create a planet and print out its details. Since at the moment Planet is just a data -holder the script won't be too exciting. - -Write the following in a file called start.php - -```php -getName()} has a population of {$planet->getPopulationSize()}.\n"; -``` - -The `new` keyword creates objects instances from classes, and automatically calls any constructor the class has. - -We can try running this now but it won't work just yet, because we need to link it up with `Planet.php`. When you type -`php start.php` you should see `PHP Fatal error: Uncaught Error: Class 'Planet' not found`. - -### Linking files together - -There are two main ways to link files together. The old way is **require_once**, and the new ways is **composer**. We'll -start with require_once. - -### Require_once - -Edit start.php to make it look like this: - -```php -getName()} has a population of {$planet->getPopulationSize()}.\n"; -``` - -Run the script again, and you should now see `Planet Neptune has a population of 0.`. The require_once statement -tells PHP to process the contents of the given file as if it had been pasted in to the current script, ignoring -the `getName()} has a population of {$planet->getPopulationSize()}.\n"; - -``` - -As long as we keep our file names matching our class names, Composer will automatically load any class we need, and -only when we need it. - -Of course technically we haven't got rid of the `require_once` statement, but by requiring the autoloader we won't -need to add any more require statements, however many classes we use in our program. - -If you run `php start.php` you should once again learn the population of Neptune. -{% endblock %} diff --git a/content/09-classes-3.md b/content/09-classes-3.md deleted file mode 100644 index 38ef1b4..0000000 --- a/content/09-classes-3.md +++ /dev/null @@ -1,128 +0,0 @@ -{% block title%}Classes, Static Methods, and Object Identity{% endblock %} - -{% block body%} - -### Static methods - -Not all the methods on a class have to run in the context of an object. Methods that work without a `$this` object -instance are called static methods. Let's add a static method to the Planet class in `src/Planet.php`: - -```php -name = $name; - $this->populationSize = $populationSize; - } - - public function getName(): string - { - return $this->name; - } - - public function getPopulationSize(): float - { - return $this->populationSize; - } - - public static function earth(): self - { - return new self('Earth', 7.7 * 10**9); - } -} -``` - - - -The `self` keyword refers to whatever class it's written in. It's more convenient to write `self` than to repeat -`Planet` many times. - -And let's edit start.php: - -```php -getName()} has a population of {$planet->getPopulationSize()}.\n"; -``` - -The double colon is the **Scope Resolution Operator**. It accesses static methods (and properties) of classes - there is -no need to have an object first. In this case our method returns an instance of the class, but it could do anything. - -### Object Identity - -A PHP variable can't actually hold an object - instead it holds an **object identifier**, also known as a **reference**. - -Two objects created the same way, with the same properties will have distinct identities. But if one object is created -and then assigned to two variables, they will both hold identifiers for the same object. This becomes important when we -make our objects mutable. This extra complexity is a good reason to prefer immutable objects, but sometimes we need -mutability. - -Let's add a function to change the world - edit `src/Planet.php` again, adding the following function inside the class: - -```php - public function receiveImmigrant(): void - { - $this->populationSize++; - } -``` - -`void` is the return type for functions that don't actually return any information. - -Now let's write a script to illustrate object identities. Enter the following in 'identities.php' - -```php -receiveImmigrant(); - -echo "Population of Mercury: {$mercury->getPopulationSize()}.\n"; -echo "Population of second Mercury: {$secondMercury->getPopulationSize()}.\n"; -echo "Population of the same Mercury: {$theSameMercury->getPopulationSize()}.\n"; -``` - -We see that `$mercury` and `$theSameMercury` are just two names for the same object, while `$secondMercury` is an -entirely separate object with its own properties, lifecycle, hopes and dreams. - -If a function accepts an object as a parameter, or returns an object, PHP doesn't make a copy of the object - it just -copies the identifier. This means that code within and without the function can access and potentially change the same -object. It's an important part of how communication happens between the parts of a PHP program, but it can easily get -confusing if not managed carefully. -{% endblock %} \ No newline at end of file diff --git a/content/10-testing.md b/content/10-testing.md deleted file mode 100644 index d2e4fec..0000000 --- a/content/10-testing.md +++ /dev/null @@ -1,149 +0,0 @@ -{% block title%}Testing{% endblock %} - -{% block body%} - -If we want to work on even a moderately complex program over time, we need automated testing - manually testing -everything every time we make a change would quickly become unsustainable. - -### Installing PHPUnit - -The leading test framework for PHP is Sebastian Bergmann's PHPUnit. - -Since we already have composer set up for our project, we can use that to install PHPUnit in the vendor directory. Run: - -``` -composer require --dev phpunit/phpunit -``` - -Composer also automatically downloads and installs all the libraries that PHPUnit depends on, and the dependencies of -its dependencies, etc. - -We use the `--dev` option because PHPUnit is a tool for developers, and not a library that our program would rely on -in production. If we wanted to prepare a copy of our program to install on a server, we would use -`composer install --no-dev` install any libraries we need and set up the autoloader but leave out PHPUnit. - -When we ran the `require` command composer.json edited our `composer.json` file to record our updated requirements. -`composer.json` should now look like: - -```json -{ - "autoload": { - "psr-4": { - "AModeratelyShortPhpTutorial\\": "src/" - } - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - } -} -``` - -The last major release of PHPUnit was 9.0, so composer has assumed we will always want whatever the latest PHPUnit -release in the 9 series is. The 10 series is not expected to be compatible with code written for PHPUnit 9, so composer -won't install that unless we edit composer json. Composer works best with dependencies that use *semantic versioning*. - -Composer has also created a new file for us, `composer.lock`. This has metadata about the exact versions of the packages -installed. At the time of writing it shows me that PHPUnit is at version 9.0.1, and I can see the details of 29 other -packages that have been installed because PHPUnit depends on them directly or indirectly. The `composer show` command -will output the list of installed packages in a much more concise format. - -### Writing a test - -Let's write our first test. Create a `test` subdirectory next to `src`, and write the following in `test/PlanetTest.php` - -```php -SUT = new Planet('planet name', 0); - } - - public function test_it_can_accept_immigrant(): void - { - $this->assertSame(0.0, $this->SUT->getPopulationSize()); - $this->SUT->receiveImmigrant(); - $this->assertSame(1.0, $this->SUT->getPopulationSize()); - } -} -``` - -The first new keyword here is `use`. This is syntactic sugar that saves us having to spell out the name of the class -`PHPUnit\Framework\TestCase` in full when we use below. - -We also meet the `extends` keyword here. This means that our class is an extension, or subclass, of PHPUnit's `TestCase` -class, which is how PHPUnit is designed to be used. If `TestCase` has been marked `final` we wouldn't be able to extend -it. - -If you don't have PHP 7.4, remember to remove the `Planet` property type of the SUT, and replace it with a docbloc as we -did for the properties of Planet itself. - -It's prudent to see a test fail at least once before believing what it says when it passes. To make it fail, comment out -`$this->populationSize++;` in `src/Planet.php`: - -`// $this->populationSize++;` - -Now run the PHPUnit command: - -```shell script -vendor/bin/phpunit test -``` - -This will search for any filenames ending in `Test.php` in the test directory. In each test case any public function -whose name starts with `test` is considered a test. For every test PHPUnit creates an instance of the class, calls the -setup function, then calls the test function, records the results, and then throws away the object. So if we had two -tests it would call `setUp` twice. Any object referenced only by garbage is garbage, so when the test case -object is thrown away the Planet is thrown away too, and any mutations to the planet will not affect the next test. - -The output from PHPUnit should look like: - -```text -PHPUnit 9.0.1 by Sebastian Bergmann and contributors. - -F 1 / 1 (100%) - -Time: 36 ms, Memory: 4.00 MB - -There was 1 failure: - -1) AModeratelyShortPhpTutorial\PlanetTest::test_it_can_accept_immigrant -Failed asserting that 0.0 is identical to 1.0. - -/tmp/composerPlayground/test/PlanetTest.php:20 - -FAILURES! -Tests: 1, Assertions: 2, Failures: 1. -``` - -Fix the Planet class putting the increment back in, and re-run the PHPUnit command. We should now see some happier -output: - -```text -PHPUnit 9.0.1 by Sebastian Bergmann and contributors. - -. 1 / 1 (100%) - -Time: 35 ms, Memory: 4.00 MB - -OK (1 test, 2 assertions) -``` - -Writing tests is a huge topic, which we can't cover in detail here. PHPUnit has excellent -[official documentation](https://phpunit.readthedocs.io/en/9.0/index.html). You might want to do -Test Driven Development (TDD) and/or Behaviour Driven Development (BDD) and write your tests before writing the -production code that they cover. - -Some other major test frameworks for PHP are [PHPSpec](http://www.phpspec.net/en/stable/) and -[Behat](https://docs.behat.org/en/latest/). These are both designed around the BDD approach, which -uses the language of *executable specifications* rather than tests. A major difference between them is that in PHPSpec, -as with PHPUnit, you code in PHP. In Behat you code in a separate language called `gherkin`, designed to look like English -and be readable by people who haven't been trained in programming. -{% endblock %} \ No newline at end of file diff --git a/content/11-databases.md b/content/11-databases.md deleted file mode 100644 index 5e9009d..0000000 --- a/content/11-databases.md +++ /dev/null @@ -1,236 +0,0 @@ -{% block title%}Databases{% endblock %} - -{% block body%} - -PHP applications often connect to databases. This is even harder to avoid in PHP than it would be in other languages -you can run on a web server like JS, Java, or C#, because PHP has a *shared-nothing* architecture. That means that the -code dealing with each request runs in isolation, and does not share any objects or variables with other requests. At -the end of the request-response cycle all stack frames are unwound and all objects become garbage. - -So if your program needs to persist anything from one page to the next, a database is a natural choice. - -For simplicity, we will use [SQLite](https://sqlite.org/index.html). This is not a database server that you connect to, -but a full featured SQL database engine supplied as a C library, and available as an add-on module for PHP, and the -world's most widely deployed DB engine. An SQLite database is just a file. - -The details of SQL, the programming language for defining and querying databases, are beyond -the scope of this tutorial. We will see some SQL code, but as far as PHP is concerned they are just strings to send to -the database engine. - -### Install the SQLite PHP Module - -We will use our SQLite database via PHP's PHP Data Objects (PDO) extension, which provides a consistent interface for -accessing many different DB types. - -First check if the necessary PHP modules are installed. Run `php -i | grep sqlite`. If you have the module set up, -you should see a line like "PDO drivers => sqlite". There may be other drivers listed alongside sqlite. - -If you don't have the PDO SQLite module, how to install it will depend on your OS and how you installed PHP. - -### Linux - -If you installed PHP through your package manager, there should be an SQLite module available in the same place. Make -sure you get the module that matches your PHP version number. For instance, on Debian or Ubuntu search your repository -with: - -`apt-cache search php | grep sqlite` - -Once you've found the package name, e.g. `php7.4-sqlite3` install it with a command such as - -`sudo apt install php7.4-sqlite3`. - -### Mac - -If you installed PHP via homebrew, I think it will automatically have come with SQLite built in and enabled. - -### Windows - -If you installed PHP as part of *XAMPP* this should have SQLite enabled by default. - -### Creating a database - -First, let's write a PHP script `src/create-database.php` to create a new database with one table: -```php -query(' - CREATE TABLE planets ( - id INTEGER PRIMARY KEY, - name text, - population_size INTEGER - ); -' -); -``` - -Run the script with `php create-database.php`. It should create a new binary file `database.sqlite`. - -`file_put_contents` writes a string to a file, overwriting the file if it already exists. We're using here to create -a completely empty file. - -The `PDO` class is supplied by the [PDO extension](https://www.php.net/manual/en/intro.pdo.php). A PDO object represents -a connection to a database, and allows us to send it queries and get back results. When we call the -[PDO constructor](https://www.php.net/manual/en/pdo.construct.php) we pass it a string holding details of the database -we want to connect to, known as a Data Source Name or *DSN*. For SQLite the DSN consists of '`sqlite:`' followed by an -absolute file path. - -### Querying the database - -Let's write a very small program to put the planets in the database, and let us view info about any planet. First we -will need a class that can insert all the planets. Make a file `src/PlanetStore.php` - -```php -connection = $connection; - } - - public function storePlanets(): void - { - $statement = $this->connection->prepare('INSERT INTO planets (name, population_size) values (?, ?);'); - foreach ($this->generatePlanets() as $planet) { - $statement->execute([$planet->getName(), $planet->getPopulationSize()]); - } - } - - /** @return Planet[] */ - private function generatePlanets(): array - { - return [ - new Planet('Mercury', 0), - new Planet('Venus', 0), - new Planet('Earth', 7.7 * 10**9), - new Planet('Mars', 0), - new Planet('Jupiter', 0), - new Planet('Saturn', 0), - new Planet('Uranus', 0), - new Planet('Neptune', 0), - ]; - } -} -``` - -Once again if you don't have PHP 7.4 you will need to remove the `PDO` property type on the `$connection` property. - -Now write a script `storePlanets.php` to use this class: - -```php -storePlanets(); -``` - -Run `php storePlanets.php`. This should add the eight planets to the database. It shouldn't produce any visible output. - -The most important thing to notice about our code so far is that we didn't concatenate any strings to create queries to -send to the database. Instead we prepared a query, using question marks as placeholders for our data, and then supplied -the values of those parameters separately each time we executed the query. - -This practice makes our database use much less error prone and more secure. The database engine knows what exactly -what's code that it should interpret and what's data that it should simply store. - -Now let's write the code to show a planet. First let's add a function to the PlanetStore class to pull a Planet out of -the database: - -```php - public function getPlanet(string $planetName): ?Planet - { - $statement = $this->connection->prepare( - 'SELECT name, population_size FROM planets where name = :name;' - ); - $statement->bindValue(':name', $planetName); - $statement->execute(); - - $row = $statement->fetch(PDO::FETCH_ASSOC); - - if (! $row) { - return null; - } - - return new Planet($row['name'], (float) $row['population_size']); - } -``` - -Notice the return type: `?Planet` means that the function may either return an instance of Planet, or the value `null`. -We can add `?` to the beginning of any type name to make it *nullable*. - -We use `(float)` to **cast** or convert the population size data from a string, as it comes back from the sqlite DB, to -a floating point number. - -In this case we have used a named parameter in the database query, instead of a question mark. - -Finally we need to make a PHP script to go between this function and the browser. Call it `viewPlanet.php`: - -```php -getPlanet((string)$_GET['name']); - -header('Content-Type: text/plain'); -if ($planet === null) { - http_response_code(404); - echo ('Planet not found'); - exit(); -} - -echo "********** {$planet->getName()} **********\n\n"; -echo "Population: {$planet->getPopulationSize()}\n"; -``` - -Notice the `$_GET` array - PHP automatically fills this so-called **superglobal** array with any query parameters sent by -the browser. It is available to us anywhere in our program, but it's best to limit where we use it - code that directly -pulls data from superglobals can be very hard to test and re-use and understand. - -Run the PHP server: -```shell script -php -S localhost:8080 -``` - -Open [http://localhost:8080/viewPlanet.php?name=Earth](http://localhost:8080/viewPlanet.php?name=Earth) in your browser. - -### Other Database Engines. - -PDO has [drivers](https://www.php.net/manual/en/pdo.drivers.php) for eleven other database engines that you can use -use instead of SQLite. Connecting, sending queries, and receiving rows should work in the same way, but of course the -details of connection strings and SQL code will vary from DB to DB. - -{% endblock %} \ No newline at end of file diff --git a/content/12-templating.md b/content/12-templating.md deleted file mode 100644 index 83b5822..0000000 --- a/content/12-templating.md +++ /dev/null @@ -1,102 +0,0 @@ -{% block title%}Templating{% endblock %} - -{% block body%} - -Our planet viewer works, but it isn't pretty. Let's use some HTML to make it look a little better. - -It has been a common practice to use PHP as a templating language, and even mix PHP and HTML code together in the same -file. While PHP can still be used as a templating language, it isn't a good one, so we won't do that. - -Instead, we will use PHP to prepare whatever data we want to show, and then pass that to a template written in a -different language. - -There are dozens of template engines available as libraries to use in PHP programs, and another good option -is to make a single page application, with all templating done in the browser, and data sent from the server as JSON. - -### Twig - -For this tutorial we will write a template in the **Twig** language, which works with PHP. - -First install Twig in your project by running: - -```shell script -composer require "twig/twig:^3.0" -``` - -Now let's make our template. Start by writing a page in HTML. For now we'll hard code everything. Make a -'`templates`' directory, and save the following as '`templates/planet.html.twig`': - -```html - - - - Some Planet - - -

Some Planet

-

Population: 34601

- - -``` - -Edit our `viewPlanet.php` file to make it use this template: - -```php -getPlanet((string)$_GET['name']); - -header('Content-Type: text/html'); -if ($planet === null) { - http_response_code(404); - echo ('Planet not found'); - exit(); -} - -$template = $twig->load('planet.html.twig'); - -echo $template->render(); -``` - -If you run the PHP server again and view your site you should see the content of the template. Of course at this point -all we're doing is serving static HTML with extra steps. - -Behind the scenes the Twig engine compiles the template to a PHP class. We don't need to read the class, but this is -useful because it means we can pass PHP objects to it and use their properties and functions. Let's pass our planet -object to the template. Change the last line of '`viewPlanet.php`' to: - -```php -echo $template->render(['planet' => $planet]); -``` - -Finally we need to edit '`planet.html.twig'`. Since we won't always be reading the template at the same time as the PHP file -that uses it, and neither will any tools and IDEs we might be using, we should add a comment to make it clear that we -expect to have a `planet` variable, and it will be of type `Planet`. Add the following to the top of the file: - -```twig -{# @var planet AModeratelyShortPhpTutorial\Planet #} -``` - -In Twig we use double curly brackets to output dynamic data: Replace `Some Planet` with -`{{'{{planet.name}}'}}`, and replace `34601` with `{{'{{planet.populationSize}}'}}`. Twig knows that we mean `getName` and -`getPopulationSize`, so we don't have to write those function names out in full. - -If you reload the page you should now see the planet details in their proper places. - -Twig is a full featured special purpose programming language, with features like loops, conditionals, filters, -inheritance, etc, but this is a PHP tutorial, not a Twig tutorial. -{% endblock %} \ No newline at end of file diff --git a/content/13-static-analysis.md b/content/13-static-analysis.md deleted file mode 100644 index edc7731..0000000 --- a/content/13-static-analysis.md +++ /dev/null @@ -1,108 +0,0 @@ -{% block title%}Static Analysis{% endblock %} - -{% block body%} - - -I recommend using at least one static analysis tool on any PHP project. While languages like TypeScript and Java have -explicit compilation phases that can quickly catch lots of errors, PHP doesn't. Most of the type checks in PHP happen only -as each line of code is executed. - -Unit tests help, but 100% test coverage is unlikely, and even then we can only ever test with a few example inputs. For -more confidence we need static analysis as well, especially if we want to be able to easily refactor our code and upgrade -the libraries we're using. - -Static Analysers for PHP include [Psalm](https://psalm.dev/), [PHPStan](https://github.com/phpstan/phpstan) and -[Phan](https://github.com/phan/phan). In this tutorial we will use Psalm. - -### Running Psalm - -Install Psalm with Composer: - -```shell script -composer require --dev vimeo/psalm -``` - -Create a Psalm Config file: - -```shell script -./vendor/bin/psalm --init -``` - -Finally, run Psalm to check your code: - -```shell script -./vendor/bin/psalm -``` - -You should see something like: - -```text -Calculating best config level based on project files -Calculating best config level based on project files -Scanning files... -Analyzing files... - -░E - -Detected level 7 as a suitable initial default -Config file created successfully. Please re-run psalm. -``` - -Re-run Psalm as instructed. If your code is the same as mine, you should see "No errors found!", and -"1 other issues found. You can display them with --show-info=true". - -That's OK, but where's the fun in a static analysis tool that doesn't complain about anything? Let's change the Psalm -settings to make it a lot stricter. Open '`psalm.xml`' and change `errorLevel="7"` to `errorLevel="1"`. - -Re-run Psalm. Now you should see an error: - - -ERROR: InvalidArgument - src/PlanetStore.php:52:27 - Argument 1 of AModeratelyShortPhpTutorial\Planet::__construct expects string, scalar provided
-        return new Planet($row['name'], (float)$row['population_size']); -
- -Psalm has looked at our code, and the code of the libraries and modules we're using, and found a mismatch. The planet -class constructor needs a string, but we've passed it something from the array returned by the -[PDOStatement::fetch](https://www.php.net/manual/en/pdostatement.fetch.php). - -Psalm knows that when we call `fetch` with `\PDO::FETCH_ASSOC` we will either get `false` or an array of scalar -values (i.e. not objects). It can see that the value isn't false at line 52, because when it is false the function -returns early, skipping that line. - -In this case we know our database a bit better than Psalm does, and we need to add a comment to tell Psalm that fetch -will return either false or an array of strings. Edit '`PlanetStore.php`' and add docblock for the `$row` variable: - -```php - /** @var array|false $row */ - $row = $statement->fetch(PDO::FETCH_ASSOC); -``` - -This docbloc means that the `$row` variable's type is the union of false and string-indexed, string-valued array. In -other words it either holds the value `false`, or it holds an array, in which any keys are strings and any values are -also strings. `|` combines types to make a **union type**. The `< >` brackets are used to provide arguments for a -**generic type**. `false` is an example of a **literal type**. - -Psalm's type system extends the PHP type system, and is a lot more expressive. PHP itself does not have union, generic, -or literal types, although unions are coming in PHP 8. We can use types that PHP doesn't support by writing them in -docblocks, with tags such as -[@var](https://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.var.pkg.html), -[@param](https://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.param.pkg.html), and -[@return](https://manual.phpdoc.org/HTMLSmartyConverter/HandS/phpDocumentor/tutorial_tags.return.pkg.html), -and making sure we run our static analysis tool every time -we edit our source code. - -Let's add a deliberate mistake to our code to see a bit more of what Psalm can help us with. Let's suppose we forgot -that the database might not have the planet we're looking for. Comment out the check for that in '`planetStore.php`': - -```php -// if (! $row) { -// return null; -// } -``` - -Re-run Psalm. It now reports two **PossiblyInvalidArrayAccess** errors, telling us that we -'`Cannot access array value on non-array variable $row of type false`'. This is a reminder that we should have made sure -that `$row` is not false before trying to use it as an array. If we leave it like this our server will produce an HTTP -500 internal server error instead of displaying the 404 page when someone asks to see the planet Pluto. Put the check -back to make Psalm happy again. -{% endblock %} \ No newline at end of file diff --git a/content/14-what-next.md b/content/14-what-next.md deleted file mode 100644 index 5993044..0000000 --- a/content/14-what-next.md +++ /dev/null @@ -1,62 +0,0 @@ -{% block title%}What next?{% endblock %} - -{% block body%} - -This has only been a cursory introduction to the PHP language and ecosystem. Here are some ideas for things to try next, -in no particular order: - -* If you enjoyed this tutorial, or found it useful please let me know and share it. If you think it could be improved, -let me know that too. You can [find me on twitter](https://twitter.com/sorsoup). You can also find -[the source code of this website](https://github.com/bdsl/phpTutorial) on GitHub. - -* Take a break from the screen - there's good evidence that you remember more of what you've learned shortly before or - after a break. - -* Write your own PHP program, or dive into a PHP codebase and make some improvement. - -* Browse the [official PHP Docs](https://www.php.net/docs.php). There's a lot in the language that we haven't covered. - -* If you find a symbol that you don't understand in PHP code, look it up in the -[What does this symbol mean in PHP?](https://stackoverflow.com/questions/3737139/reference-what-does-this-symbol-mean-in-php) - Stack Overflow reference page. - -* Learn about PDO in depth with [(The only proper) PDO tutorial](https://phpdelusions.net/pdo). - -* Read the [Twig Documentation](https://twig.symfony.com/doc/3.x/) and learn how to make more sophisticated templates. - -* Watch the talk [Aggressive PHP Quality Assurance in 2019](https://www.youtube.com/watch?v=8rdTSYljts4) by [Marco -Pivetta](https://twitter.com/Ocramius), to learn that "*PHP is a horrible language that allows you to do too many -things*", and how to use the tools that will tell you not to do the bad things. In this tutorial I've tried to show only -the good things you can do in PHP. - -* Learn how to use a PHP Framework or CMS. I would personally recommend Symfony, and the -[Symfony Getting Started](https://symfony.com/doc/current/index.html#gsc.tab=0) guide. - - You could also try the -[Laravel Quickstart Guide](https://laravel.com/docs/5.1/quickstart), or a CMS - perhaps -[First Steps with WordPress](https://wordpress.org/support/article/first-steps-with-wordpress-b/) or the -[Drupal 8 Documentation](https://www.drupal.org/docs/8). - -* Read [PHP: The Right Way](https://phptherightway.com/). This covers a lot more topics than we've been able to include -here, including a lot of the history of how PHP got to be the language it is today. - -* Browse PHP packages on [Packagist](https://packagist.org/). This is where Composer looks by default when you tell it -to install a library or tool. - -* Subscribe to [PHP Weekly News](http://www.phpweekly.com/) for interesting links every Thursday. - -* [Subscribe](https://info.jetbrains.com/PHP-Annotated-Subscription.html) to -[PHP Annotated Monthly](https://blog.jetbrains.com/phpstorm/category/php-annotated-monthly/) from the makers of -[PhpStorm](https://www.jetbrains.com/phpstorm/), for interesting links every month. - -* And finally, read [Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing -Technology Organizations](https://itrevolution.com/book/accelerate/), by [Nicole Forsgren](https://twitter.com/nicolefv), -[Jez Humble](https://twitter.com/jezhumble) -and [Gene Kim](https://twitter.com/RealGeneKim). This isn't about PHP, but I think it's perhaps -the most important book for anyone making software in an organisation to read. - - *Accelerate* describes the capabilities that -organisations need to develop and maintain to perform well with software. You can also read about those capabilities in -the briefer and slightly more up-to date form of the -[2019 Accelerate State of DevOps Report](https://cloud.google.com/devops/state-of-devops/). -{% endblock %} diff --git a/diferentiation.md b/diferentiation.md deleted file mode 100644 index 961541b..0000000 --- a/diferentiation.md +++ /dev/null @@ -1,41 +0,0 @@ -# Differentiating this from other resources - -To motivate work on writing this tutorial, it should ideally offer something different to (but not necessarily better -than) all preexisting resources. - -Various resources were suggested in comments at -https://www.reddit.com/r/PHP/comments/f5ix6n/anyone_know_a_good_php_as_a_second_language/ : - -## PHP The Right Way - -See [intro](content/01-introduction.md). - -## Laracasts - The PHP Practitioner - -Looks like it covers a lot of the same stuff I want to cover, but with different approach. Most obviously this will -be a text based tutorial for readers to use at their own pace, not a series of videos. Laracasts covers mixing HTML -with PHP which I plan to skip. Doesn't use strict types in code examples. Final section is about Laravel, which I -disprefer. I don't plan to include anything on how to use a framework but will include something about them in a -'what next' section at the end, perhaps including links to https://symfony.com/doc/current/setup.html -and https://symfony.com/doc/current/create_framework/index.html - -Although the course is free the Laracasts website has lots of paid content, and the site overall feels quite focused -on selling subscriptions. - -# Derek Banas on YouTube - -Video at https://www.youtube.com/watch?v=7TF00hJI78Y - -Video is old - from 2014. Again mixes PHP and HTML. - -# Learn X in Y minutes - -https://learnxinyminutes.com/docs/php/ - -Again this is not focused on contemporary PHP - it "describes PHP 5+". I want to describe PHP 7.2+, if I don't decide -to just do 7.4 only. I do like the terse format - very quick to read through, but this is aiming to a be a bit gentler. - -# Official Documentation - -See [intro](content/01-introduction.md) again. Tutorial part is much too minimal, docs as a whole go into much more -detail than desired here. diff --git a/generate.php b/generate.php deleted file mode 100755 index 215dd2e..0000000 --- a/generate.php +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env php -deleteDir('generated'); -$fileystem->createDir('generated'); -$fileystem->write('generated/.nojekyll', ''); // stops Github running Jekyll site generator - -$layoutTwig = new \Twig\Environment(new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates')); -$contentTwig = new \Twig\Environment(new \Twig\Loader\FilesystemLoader(__DIR__ . '/content')); - -/** @var list $files */ -$files = $fileystem->listContents('/content'); - -$processedFiles = []; -$pagcounter = 0; -foreach ($files as $file) -{ - $pagcounter++; - $markdown = $contentTwig->load($file['basename'])->renderBlock('body'); - - $processedFile = $file; - /** @var string $htmlContent */ - $htmlContent = (new Parsedown())->text($markdown); - $processedFile['htmlContent'] = $htmlContent; - $processedFile['title'] = $contentTwig->load($file['basename'])->renderBlock('title'); - $processedFile['pageNumber'] = $pagcounter; - - $processedFiles[] = $processedFile; -} - -foreach ($processedFiles as $index => $file) -{ - $fileystem->write( - 'generated/' . str_replace('.md', '.html', $file['basename']), - $layoutTwig->render('layout.html.twig', - [ - 'pages' => $processedFiles, - 'body' => $file['htmlContent'], - 'pageNumber' => $file['pageNumber'], - 'title' => $file['title'], - 'previousPage' => $processedFiles[$index-1] ?? null, - 'nextPage' => $processedFiles[$index+1] ?? null, - ]) - ); -} - -$fileystem->write('generated/CNAME', "a-moderately-short-php-tutorial.com\n"); - -$fileystem->copy('generated/01-introduction.html', 'generated/index.html'); - -$fileystem->copy('css/styles.css', 'generated/css/styles.css'); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..947a6ff --- /dev/null +++ b/index.html @@ -0,0 +1,118 @@ + + + + + + + + Introduction | A Moderately Short PHP Tutorial + + + + + + + + + + + +
+

+ A Moderately Short PHP Tutorial + +

+ + +
+

Introduction

+

Who is this for?

+

This is for anyone who wants to learn to program in PHP.

+

You should probably already know at least one programming language with curly braces, if conditions, variables, functions and +loops. The most likely scenario is that you are a Javascript or Typescript developer, working on a team that uses +a combination of PHP and JS or TS, and while you might not currently need deep PHP knowledge, +you want 'T-shaped skills', involving basic PHP knowledge. There will be some comparisons with Javascript and +Typescript.

+

Why did I write it?

+

I work as a PHP developer, and have some colleagues with great Javascript skills who sometimes want to do work in the PHP +side of our application, but don't have PHP experience from their previous jobs. I looked around for an introductory +tutorial to recommend they can work through, but I didn't find anything that seemed exactly right.

+

Approach

+

This is a step-by-step guide to using a selection of the most important aspects of PHP. It doesn't attempt to be +comprehensive.

+

PHP is an old language that has evolved organically over the years. There are many ways to do things, +and many parts of the language that are best avoided in new code. This tutorial will concentrate on recommended +ways to use PHP, and ignore most of the things that PHP allows you to do but you probably shouldn't.

+
+ + + + + +
+ + + + +
+ + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index adb1769..0000000 --- a/psalm.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - diff --git a/readme.md b/readme.md deleted file mode 100644 index ea14392..0000000 --- a/readme.md +++ /dev/null @@ -1,12 +0,0 @@ -# Basic PHP tutorial - -(c) Barney Laurance 2020 - -I'm writing an introductory PHP tutorial here, as there doesn't seem to be anything I'm really happy -recommending already free online in this sort of format. - -Some more notes on how this is should be different to what's already out there are in [diferentiation.md](./diferentiation.md). - -Read the website at https://a-moderately-short-php-tutorial.com/ - -CircleCI is [configured](./.circleci/config.yml) to to rebuild the website based on any changes in the repository. diff --git a/sample-project/.gitignore b/sample-project/.gitignore deleted file mode 100644 index 7579f74..0000000 --- a/sample-project/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -vendor -composer.lock diff --git a/sample-project/composer.json b/sample-project/composer.json deleted file mode 100644 index 18c5b1f..0000000 --- a/sample-project/composer.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "require": { - "twig/twig": "^3.0" - }, - "autoload": { - "psr-4": { - "AModeratelyShortPhpTutorial\\": "src/" - } - } -} diff --git a/sample-project/create-database.php b/sample-project/create-database.php deleted file mode 100644 index 3a63a26..0000000 --- a/sample-project/create-database.php +++ /dev/null @@ -1,17 +0,0 @@ -query(<< - - - - - - - - - diff --git a/sample-project/src/Planet.php b/sample-project/src/Planet.php deleted file mode 100644 index 5bcd023..0000000 --- a/sample-project/src/Planet.php +++ /dev/null @@ -1,31 +0,0 @@ -name = $name; - $this->populationSize = $populationSize; - } - - public function getName(): string - { - return $this->name; - } - - public function getPopulationSize(): float - { - return $this->populationSize; - } - - public static function earth(): self - { - return new self('Earth', 7.7 * 10**9); - } -} diff --git a/sample-project/src/PlanetStore.php b/sample-project/src/PlanetStore.php deleted file mode 100644 index 90d14b4..0000000 --- a/sample-project/src/PlanetStore.php +++ /dev/null @@ -1,55 +0,0 @@ -connection = $connection; - } - - public function storePlanets(): void - { - $statement = $this->connection->prepare('INSERT INTO planets (name, population_size) values (?, ?);'); - foreach ($this->generatePlanets() as $planet) { - $statement->execute([$planet->getName(), $planet->getPopulationSize()]); - } - } - - /** @return Planet[] */ - private function generatePlanets(): array - { - return [ - new Planet('Mercury', 0), - new Planet('Venus', 0), - new Planet('Earth', 7.7 * 10**9), - new Planet('Mars', 0), - new Planet('Jupiter', 0), - new Planet('Saturn', 0), - new Planet('Uranus', 0), - new Planet('Neptune', 0), - ]; - } - - public function getPlanet(string $planetName): ?Planet - { - $statement = $this->connection->prepare( - 'SELECT name, population_size FROM planets where name = :name;' - ); - $statement->bindValue(':name', $planetName); - $statement->execute(); - - /** @var array|false $row */ - $row = $statement->fetch(PDO::FETCH_ASSOC); - - if (! $row) { - return null; - } - - return new Planet($row['name'], (float)$row['population_size']); - } -} diff --git a/sample-project/storePlanets.php b/sample-project/storePlanets.php deleted file mode 100644 index 498e541..0000000 --- a/sample-project/storePlanets.php +++ /dev/null @@ -1,13 +0,0 @@ -storePlanets(); diff --git a/sample-project/templates/planet.html.twig b/sample-project/templates/planet.html.twig deleted file mode 100644 index ff36130..0000000 --- a/sample-project/templates/planet.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{# @var planet AModeratelyShortPhpTutorial\Planet #} - - - - {{planet.name}} - - -

{{planet.name}}

-

Population: {{planet.populationSize}}

- -

This is page was generated from a Twig template.

- - diff --git a/sample-project/viewPlanet.php b/sample-project/viewPlanet.php deleted file mode 100644 index 778513f..0000000 --- a/sample-project/viewPlanet.php +++ /dev/null @@ -1,24 +0,0 @@ -getPlanet((string)$_GET['name']); - -header('Content-Type: text/plain'); -if ($planet === null) { - http_response_code(404); - echo ('Planet not found'); - exit(); -} - -echo "********** {$planet->getName()} **********\n\n"; -echo "Population: {$planet->getPopulationSize()}\n"; diff --git a/templates/layout.html.twig b/templates/layout.html.twig deleted file mode 100644 index 882f62f..0000000 --- a/templates/layout.html.twig +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - {{ title }} | A Moderately Short PHP Tutorial - - - - - - - - - - - -
-

- A Moderately Short PHP Tutorial - -

- -
- -
-
-

{{ title }}

- {{ body | raw }} -
- - - - - -
- - - - -
- - diff --git a/test.sh b/test.sh deleted file mode 100755 index 7013f61..0000000 --- a/test.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -x -set -e - -if [[ $(cat *.md content/*.md | aspell --run-together --run-together-limit=3 --lang=en_GB.UTF-8 -p ./.spelling/wordlist list | sort | uniq ) ]]; then - echo "Spelling issue found - check spellings of word(s) shown above. If words are correct add them to .spelling/wordlist" - exit 1 -fi diff --git a/todo.md b/todo.md deleted file mode 100644 index e752aa5..0000000 --- a/todo.md +++ /dev/null @@ -1,12 +0,0 @@ -# To do - -- make list of topics to cover and exclude -- choose which keywords to cover -- Write the thing -- Evaluate and choose flat-file CMS / static site generator -- Generate HTML website -- Make build script check PHP snippets shown in the content and assert that they output what I think they output. - -- add something about associative (string-keyed) arrays in array page. - -etc etc diff --git a/topics.md b/topics.md deleted file mode 100644 index 59c01e3..0000000 --- a/topics.md +++ /dev/null @@ -1,95 +0,0 @@ -#Outline of possible topics to cover - -Not in any specific order. Topics for which at least some writing is done are marked with *. - -* Intro * -* Getting PHP * -* Hello World * -* Http * -* Variables * -* Arrays * -* Functions * -* Classes * -* Loading other files (require_once and / or using composer autoloader) - including namespaces * -* Databases (PDO with SQLite) * -* Testing (PHPUnit) * -* Static analysis (Psalm) * -* Templating (e.g. twig) * -* How to learn / do more with PHP after tutorial (including links to frameworks, official docs, php weekly / annotated etc) * - -## PHP Keywords -Full list of keywords from https://www.php.net/manual/en/reserved.keywords.php - -Some keywords should be introduced by this tutorial (yes), some should be left out as not necessary -for a beginning PHP developer, and some should be assumed known from JS/TS or other languages. - -| Keyword | Introduce (yes/no/assume/?) | -| --------------------------------- | ---------------------- | -| __halt_compiler() | No | -| abstract | No | -| and | No | -| array() | No | -| as | Yes | -| break | ? | -| callable (as of PHP 5.4) | ? | -| case | No | -| catch | Yes | -| class | Yes | -| clone | ? | -| const | Yes | -| continue | ? | -| declare | Yes | -| default | ? | -| die() | ? | -| do | No | -| echo | Yes | -| else | assume | -| elseif | no | -| empty() | ? | -| enddeclare | no | -| endfor | no | -| endforeach | no | -| endif | no | -| endswitch | no | -| endwhile | no | -| eval() | no | -| exit() | no | -| extends | Yes (?) | -| final | Yes | -| finally (as of PHP 5.5) | no | -| for | no | -| foreach | yes | -| function | yes | -| global | no | -| goto (as of PHP 5.3) | no | -| if | assume | -| implements | yes | -| include | no | -| include_once | no | -| instanceof | no | -| insteadof (as of PHP 5.4) | no | -| interface | yes | -| isset() | no | -| list() | no | -| namespace (as of PHP 5.3) | yes | -| new | yes | -| or | no | -| print | no | -| private | yes | -| protected | no | -| public | yes | -| require | no | -| require_once | yes (at least to require vendor/autoload.php ) | -| return | assume | -| static | yes | -| switch | no / assume | -| throw | yes | -| trait (as of PHP 5.4) | no | -| try | yes | -| unset() | no | -| use | ? | -| var | no | -| while | no | -| xor | no | -| yield (as of PHP 5.5) | no | -| yield from (as of PHP 7.0) | no |