16 Commits d02cdf2196 ... eedf5e7312

Author SHA1 Message Date
  Frédéric G. MARAND eedf5e7312 Merge branch '07-acronym' 1 month ago
  Frederic G. MARAND 8b852e0fe4 06: acronym. 2 months ago
  Frederic G. MARAND d02cdf2196 06: acronym. 2 months ago
  Frédéric G. MARAND 8d64dff1e6 Merge branch '06-list_ops' 2 months ago
  Frédéric G. MARAND f12a7a37d7 06 List ops. 2 months ago
  Frédéric G. MARAND c285451aff Merge branch '05-protein_translation' 2 months ago
  Frédéric G. MARAND 72d56c50c2 05 Protein translation. 2 months ago
  Frédéric G. MARAND fb7e024f3c Merge branch '04-roman_numerals' 2 months ago
  Frédéric G. MARAND e83b708b98 04 Roman numerals. 2 months ago
  Frédéric G. MARAND 8e0958cc15 Merge branch '03-raindrops' 2 months ago
  Frédéric G. MARAND 8ac99831ee 03 Raindrops. 2 months ago
  Frédéric G. MARAND e1e9c8aa88 Merge branch '02-reverse-string' 3 months ago
  Frédéric G. MARAND 4e4cceda39 02 Reverse string with multibyte support added. 3 months ago
  Frédéric G. MARAND 7ad9bf4fea Merge branch '01-leap' 3 months ago
  Frédéric G. MARAND 71b3a55948 01 Leap. 3 months ago
  Frédéric G. MARAND c3f4ab2642 Ignore .idea and generated files. 3 months ago

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 /.idea
 /vendor
+.phpunit.result.cache
 *.cache

+ 3 - 1
composer.json

@@ -1,6 +1,8 @@
 {
     "require-dev": {
-        "phpunit/phpunit": "^10",
         "psy/psysh": "^0.12.0"
+    },
+    "require": {
+        "phpunit/phpunit": "^10.5.16"
     }
 }

+ 251 - 242
composer.lock

@@ -4,9 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "64ac7bf6217b00af9d819f3c847a31df",
-    "packages": [],
-    "packages-dev": [
+    "content-hash": "c943050201d8edfc9041ef834eb14b98",
+    "packages": [
         {
             "name": "myclabs/deep-copy",
             "version": "1.11.1",
@@ -68,16 +67,16 @@
         },
         {
             "name": "nikic/php-parser",
-            "version": "v5.0.1",
+            "version": "v5.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69"
+                "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2218c2252c874a4624ab2f613d86ac32d227bc69",
-                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13",
+                "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13",
                 "shasum": ""
             },
             "require": {
@@ -120,26 +119,27 @@
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.1"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2"
             },
-            "time": "2024-02-21T19:24:10+00:00"
+            "time": "2024-03-05T20:51:40+00:00"
         },
         {
             "name": "phar-io/manifest",
-            "version": "2.0.3",
+            "version": "2.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "97803eca37d319dfa7826cc2437fc020857acb53"
+                "reference": "54750ef60c58e43759730615a392c31c80e23176"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53",
-                "reference": "97803eca37d319dfa7826cc2437fc020857acb53",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+                "reference": "54750ef60c58e43759730615a392c31c80e23176",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
+                "ext-libxml": "*",
                 "ext-phar": "*",
                 "ext-xmlwriter": "*",
                 "phar-io/version": "^3.0.1",
@@ -180,9 +180,15 @@
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
             "support": {
                 "issues": "https://github.com/phar-io/manifest/issues",
-                "source": "https://github.com/phar-io/manifest/tree/2.0.3"
+                "source": "https://github.com/phar-io/manifest/tree/2.0.4"
             },
-            "time": "2021-07-20T11:28:43+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-03T12:33:53+00:00"
         },
         {
             "name": "phar-io/version",
@@ -237,16 +243,16 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "10.1.11",
+            "version": "10.1.14",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "78c3b7625965c2513ee96569a4dbb62601784145"
+                "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/78c3b7625965c2513ee96569a4dbb62601784145",
-                "reference": "78c3b7625965c2513ee96569a4dbb62601784145",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b",
+                "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b",
                 "shasum": ""
             },
             "require": {
@@ -303,7 +309,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
                 "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.11"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14"
             },
             "funding": [
                 {
@@ -311,7 +317,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-21T15:38:30+00:00"
+            "time": "2024-03-12T15:33:41+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
@@ -558,16 +564,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "10.5.11",
+            "version": "10.5.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4"
+                "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4",
-                "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd",
+                "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd",
                 "shasum": ""
             },
             "require": {
@@ -639,7 +645,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.11"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.16"
             },
             "funding": [
                 {
@@ -655,152 +661,20 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-02-25T14:05:00+00:00"
-        },
-        {
-            "name": "psr/container",
-            "version": "2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/php-fig/container.git",
-                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
-                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.4.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.0.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Psr\\Container\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "PHP-FIG",
-                    "homepage": "https://www.php-fig.org/"
-                }
-            ],
-            "description": "Common Container Interface (PHP FIG PSR-11)",
-            "homepage": "https://github.com/php-fig/container",
-            "keywords": [
-                "PSR-11",
-                "container",
-                "container-interface",
-                "container-interop",
-                "psr"
-            ],
-            "support": {
-                "issues": "https://github.com/php-fig/container/issues",
-                "source": "https://github.com/php-fig/container/tree/2.0.2"
-            },
-            "time": "2021-11-05T16:47:00+00:00"
-        },
-        {
-            "name": "psy/psysh",
-            "version": "v0.12.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/750bf031a48fd07c673dbe3f11f72362ea306d0d",
-                "reference": "750bf031a48fd07c673dbe3f11f72362ea306d0d",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-tokenizer": "*",
-                "nikic/php-parser": "^5.0 || ^4.0",
-                "php": "^8.0 || ^7.4",
-                "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
-                "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
-            },
-            "conflict": {
-                "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
-            },
-            "require-dev": {
-                "bamarni/composer-bin-plugin": "^1.2"
-            },
-            "suggest": {
-                "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
-                "ext-pdo-sqlite": "The doc command requires SQLite to work.",
-                "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well."
-            },
-            "bin": [
-                "bin/psysh"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "0.12.x-dev"
-                },
-                "bamarni-bin": {
-                    "bin-links": false,
-                    "forward-command": false
-                }
-            },
-            "autoload": {
-                "files": [
-                    "src/functions.php"
-                ],
-                "psr-4": {
-                    "Psy\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Justin Hileman",
-                    "email": "justin@justinhileman.info",
-                    "homepage": "http://justinhileman.com"
-                }
-            ],
-            "description": "An interactive shell for modern PHP.",
-            "homepage": "http://psysh.org",
-            "keywords": [
-                "REPL",
-                "console",
-                "interactive",
-                "shell"
-            ],
-            "support": {
-                "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.12.0"
-            },
-            "time": "2023-12-20T15:28:09+00:00"
+            "time": "2024-03-28T10:08:10+00:00"
         },
         {
             "name": "sebastian/cli-parser",
-            "version": "2.0.0",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/cli-parser.git",
-                "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae"
+                "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae",
-                "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae",
+                "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+                "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
                 "shasum": ""
             },
             "require": {
@@ -835,7 +709,8 @@
             "homepage": "https://github.com/sebastianbergmann/cli-parser",
             "support": {
                 "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
-                "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0"
+                "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+                "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
             },
             "funding": [
                 {
@@ -843,7 +718,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-02-03T06:58:15+00:00"
+            "time": "2024-03-02T07:12:49+00:00"
         },
         {
             "name": "sebastian/code-unit",
@@ -1093,16 +968,16 @@
         },
         {
             "name": "sebastian/diff",
-            "version": "5.1.0",
+            "version": "5.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f"
+                "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
-                "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
+                "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
                 "shasum": ""
             },
             "require": {
@@ -1110,7 +985,7 @@
             },
             "require-dev": {
                 "phpunit/phpunit": "^10.0",
-                "symfony/process": "^4.2 || ^5"
+                "symfony/process": "^6.4"
             },
             "type": "library",
             "extra": {
@@ -1148,7 +1023,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/diff/issues",
                 "security": "https://github.com/sebastianbergmann/diff/security/policy",
-                "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0"
+                "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
             },
             "funding": [
                 {
@@ -1156,20 +1031,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-12-22T10:55:06+00:00"
+            "time": "2024-03-02T07:15:17+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "6.0.1",
+            "version": "6.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951"
+                "reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951",
-                "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
+                "reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
                 "shasum": ""
             },
             "require": {
@@ -1184,7 +1059,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "6.0-dev"
+                    "dev-main": "6.1-dev"
                 }
             },
             "autoload": {
@@ -1212,7 +1087,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/environment/issues",
                 "security": "https://github.com/sebastianbergmann/environment/security/policy",
-                "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1"
+                "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
             },
             "funding": [
                 {
@@ -1220,20 +1095,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-04-11T05:39:26+00:00"
+            "time": "2024-03-23T08:47:14+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "5.1.1",
+            "version": "5.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc"
+                "reference": "955288482d97c19a372d3f31006ab3f37da47adf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc",
-                "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf",
+                "reference": "955288482d97c19a372d3f31006ab3f37da47adf",
                 "shasum": ""
             },
             "require": {
@@ -1290,7 +1165,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/exporter/issues",
                 "security": "https://github.com/sebastianbergmann/exporter/security/policy",
-                "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1"
+                "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2"
             },
             "funding": [
                 {
@@ -1298,20 +1173,20 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-09-24T13:22:09+00:00"
+            "time": "2024-03-02T07:17:12+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "6.0.1",
+            "version": "6.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4"
+                "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4",
-                "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+                "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
                 "shasum": ""
             },
             "require": {
@@ -1345,14 +1220,14 @@
                 }
             ],
             "description": "Snapshotting of global state",
-            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "homepage": "https://www.github.com/sebastianbergmann/global-state",
             "keywords": [
                 "global state"
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/global-state/issues",
                 "security": "https://github.com/sebastianbergmann/global-state/security/policy",
-                "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1"
+                "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
             },
             "funding": [
                 {
@@ -1360,7 +1235,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2023-07-19T07:19:23+00:00"
+            "time": "2024-03-02T07:19:19+00:00"
         },
         {
             "name": "sebastian/lines-of-code",
@@ -1704,6 +1579,190 @@
             ],
             "time": "2023-02-07T11:34:05+00:00"
         },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git",
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues",
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-03T12:36:25+00:00"
+        }
+    ],
+    "packages-dev": [
+        {
+            "name": "psr/container",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+                "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/2.0.2"
+            },
+            "time": "2021-11-05T16:47:00+00:00"
+        },
+        {
+            "name": "psy/psysh",
+            "version": "v0.12.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bobthecow/psysh.git",
+                "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
+                "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-tokenizer": "*",
+                "nikic/php-parser": "^5.0 || ^4.0",
+                "php": "^8.0 || ^7.4",
+                "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
+                "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
+            },
+            "conflict": {
+                "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.2"
+            },
+            "suggest": {
+                "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
+                "ext-pdo-sqlite": "The doc command requires SQLite to work.",
+                "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well."
+            },
+            "bin": [
+                "bin/psysh"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "0.12.x-dev"
+                },
+                "bamarni-bin": {
+                    "bin-links": false,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions.php"
+                ],
+                "psr-4": {
+                    "Psy\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Justin Hileman",
+                    "email": "justin@justinhileman.info",
+                    "homepage": "http://justinhileman.com"
+                }
+            ],
+            "description": "An interactive shell for modern PHP.",
+            "homepage": "http://psysh.org",
+            "keywords": [
+                "REPL",
+                "console",
+                "interactive",
+                "shell"
+            ],
+            "support": {
+                "issues": "https://github.com/bobthecow/psysh/issues",
+                "source": "https://github.com/bobthecow/psysh/tree/v0.12.3"
+            },
+            "time": "2024-04-02T15:57:53+00:00"
+        },
         {
             "name": "symfony/console",
             "version": "v7.0.4",
@@ -2365,56 +2424,6 @@
                 }
             ],
             "time": "2024-02-15T11:33:06+00:00"
-        },
-        {
-            "name": "theseer/tokenizer",
-            "version": "1.2.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
-                "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
-                "shasum": ""
-            },
-            "require": {
-                "ext-dom": "*",
-                "ext-tokenizer": "*",
-                "ext-xmlwriter": "*",
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Arne Blankerts",
-                    "email": "arne@blankerts.de",
-                    "role": "Developer"
-                }
-            ],
-            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
-            "support": {
-                "issues": "https://github.com/theseer/tokenizer/issues",
-                "source": "https://github.com/theseer/tokenizer/tree/1.2.2"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/theseer",
-                    "type": "github"
-                }
-            ],
-            "time": "2023-11-20T00:12:19+00:00"
         }
     ],
     "aliases": [],

+ 25 - 0
leap/.exercism/config.json

@@ -0,0 +1,25 @@
+{
+  "authors": [],
+  "contributors": [
+    "arueckauer",
+    "dkinzer",
+    "kunicmarko20",
+    "kytrinyx",
+    "lafent",
+    "petemcfarlane"
+  ],
+  "files": {
+    "solution": [
+      "Leap.php"
+    ],
+    "test": [
+      "LeapTest.php"
+    ],
+    "example": [
+      ".meta/example.php"
+    ]
+  },
+  "blurb": "Determine whether a given year is a leap year.",
+  "source": "CodeRanch Cattle Drive, Assignment 3",
+  "source_url": "https://coderanch.com/t/718816/Leap"
+}

+ 1 - 0
leap/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"leap","id":"37e0403cf2cc4604a550bba43110d608","url":"https://exercism.org/tracks/php/exercises/leap","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
leap/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit Leap.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 39 - 0
leap/Leap.php

@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+function isLeap(int $year): bool
+{
+    if ($year % 400 == 0) {
+      return true;
+    }
+    if ($year % 100 == 0) {
+      return false;
+    }
+    if ($year % 4 != 0) {
+      return false;
+    }
+    return true;
+}

+ 58 - 0
leap/LeapTest.php

@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class LeapTest extends PHPUnit\Framework\TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'Leap.php';
+    }
+
+    public function testLeapYear(): void
+    {
+        $this->assertTrue(isLeap(1996));
+    }
+
+    public function testNonLeapYear(): void
+    {
+        $this->assertFalse(isLeap(1997));
+    }
+
+    public function testNonLeapEvenYear(): void
+    {
+        $this->assertFalse(isLeap(1998));
+    }
+
+    public function testCentury(): void
+    {
+        $this->assertFalse(isLeap(1900));
+    }
+
+    public function testFourthCentury(): void
+    {
+        $this->assertTrue(isLeap(2400));
+    }
+}

+ 40 - 0
leap/README.md

@@ -0,0 +1,40 @@
+# Leap
+
+Welcome to Leap on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Introduction
+
+A leap year (in the Gregorian calendar) occurs:
+
+- In every year that is evenly divisible by 4.
+- Unless the year is evenly divisible by 100, in which case it's only a leap year if the year is also evenly divisible by 400.
+
+Some examples:
+
+- 1997 was not a leap year as it's not divisible by 4.
+- 1900 was not a leap year as it's not divisible by 400.
+- 2000 was a leap year!
+
+~~~~exercism/note
+For a delightful, four-minute explanation of the whole phenomenon of leap years, check out [this YouTube video](https://www.youtube.com/watch?v=xX96xng7sAE).
+~~~~
+
+## Instructions
+
+Your task is to determine whether a given year is a leap year.
+
+## Source
+
+### Contributed to by
+
+- @arueckauer
+- @dkinzer
+- @kunicmarko20
+- @kytrinyx
+- @lafent
+- @petemcfarlane
+
+### Based on
+
+CodeRanch Cattle Drive, Assignment 3 - https://coderanch.com/t/718816/Leap

+ 17 - 0
list-ops/.exercism/config.json

@@ -0,0 +1,17 @@
+{
+  "authors": [
+    "homersimpsons"
+  ],
+  "files": {
+    "solution": [
+      "ListOps.php"
+    ],
+    "test": [
+      "ListOpsTest.php"
+    ],
+    "example": [
+      ".meta/example.php"
+    ]
+  },
+  "blurb": "Implement basic list operations."
+}

+ 1 - 0
list-ops/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"list-ops","id":"b8af1c9c5e76467c9e627ed45df75100","url":"https://exercism.org/tracks/php/exercises/list-ops","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
list-ops/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit ListOps.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 83 - 0
list-ops/ListOps.php

@@ -0,0 +1,83 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class ListOps {
+
+  public function append(array $list1, array $list2): array {
+    $list = $list1;
+    foreach ($list2 as $item) {
+      $list[] = $item;
+    }
+    return $list;
+  }
+
+  public function concat(array $list1, array ...$listn): array {
+    $res = $list1;
+    foreach ($listn as $list) {
+      foreach ($list as $item) {
+        $res[] = $item;
+      }
+    }
+    return $res;
+  }
+
+  /**
+   * @param callable(mixed $item): bool $predicate
+   */
+  public function filter(callable $predicate, array $list): array {
+    return array_values(array_filter($list, $predicate));
+  }
+
+  public function length(array $list): int {
+    return count($list);
+  }
+
+  /**
+   * @param callable(mixed $item): mixed $function
+   */
+  public function map(callable $function, array $list): array {
+    return array_map($function, $list);
+  }
+
+  /**
+   * @param callable(mixed $accumulator, mixed $item): mixed $function
+   */
+  public function foldl(callable $function, array $list, $accumulator) {
+    return array_reduce($list, $function, $accumulator);
+  }
+
+  /**
+   * @param callable(mixed $accumulator, mixed $item): mixed $function
+   */
+  public function foldr(callable $function, array $list, $accumulator) {
+    return array_reduce(array_reverse($list), $function, $accumulator);
+  }
+
+  public function reverse(array $list): array {
+    return array_reverse($list);
+  }
+
+}

+ 263 - 0
list-ops/ListOpsTest.php

@@ -0,0 +1,263 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+use PHPUnit\Framework\ExpectationFailedException;
+
+class ListOpsTest extends PHPUnit\Framework\TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'ListOps.php';
+    }
+
+    /**
+     * @testdox append entries to a list and return the new list -> empty lists
+     */
+    public function testAppendEmptyLists()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([], $listOps->append([], []));
+    }
+
+    /**
+     * @testdox append entries to a list and return the new list -> list to empty list
+     */
+    public function testAppendNonEmptyListToEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([1, 2, 3, 4], $listOps->append([1, 2, 3, 4], []));
+    }
+
+    /**
+     * @testdox append entries to a list and return the new list -> empty list to list
+     */
+    public function testAppendEmptyListToNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([1, 2, 3, 4], $listOps->append([], [1, 2, 3, 4]));
+    }
+
+    /**
+     * @testdox append entries to a list and return the new list -> non-empty lists
+     */
+    public function testAppendNonEmptyLists()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([1, 2, 2, 3, 4, 5], $listOps->append([1, 2], [2, 3, 4, 5]));
+    }
+
+    /**
+     * @testdox concatenate a list of lists -> empty list
+     */
+    public function testConcatEmptyLists()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([], $listOps->concat([], []));
+    }
+
+    /**
+     * @testdox concatenate a list of lists -> list of lists
+     */
+    public function testConcatLists()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([1, 2, 3, 4, 5, 6], $listOps->concat([1, 2], [3], [], [4, 5, 6]));
+    }
+
+    /**
+     * @testdox concatenate a list of lists -> list of nested lists
+     */
+    public function testConcatNestedLists()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([[1], [2], [3], [], [4, 5, 6]], $listOps->concat([[1], [2]], [[3]], [[]], [[4, 5, 6]]));
+    }
+
+    /**
+     * @testdox filter list returning only values that satisfy the filter function -> empty list
+     */
+    public function testFilterEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            [],
+            $listOps->filter(static fn ($el) => $el % 2 === 1, [])
+        );
+    }
+
+    /**
+     * @testdox filter list returning only values that satisfy the filter function -> non empty list
+     */
+    public function testFilterNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            [1, 3, 5],
+            $listOps->filter(static fn ($el) => $el % 2 === 1, [1, 2, 3, 5])
+        );
+    }
+
+    /**
+     * @testdox returns the length of a list -> empty list
+     */
+    public function testLengthEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(0, $listOps->length([]));
+    }
+
+    /**
+     * @testdox returns the length of a list -> non-empty list
+     */
+    public function testLengthNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(4, $listOps->length([1, 2, 3, 4]));
+    }
+
+    /**
+     * @testdox returns a list of elements whose values equal the list value transformed by the mapping function -> empty list
+     */
+    public function testMapEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            [],
+            $listOps->map(static fn ($el) => $el + 1, [])
+        );
+    }
+
+    /**
+     * @testdox returns a list of elements whose values equal the list value transformed by the mapping function -> non-empty list
+     */
+    public function testMapNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            [2, 4, 6, 8],
+            $listOps->map(static fn ($el) => $el + 1, [1, 3, 5, 7])
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the left with a function -> empty list
+     */
+    public function testFoldlEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            2,
+            $listOps->foldl(static fn ($acc, $el) => $el * $acc, [], 2)
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the left with a function -> direction independent function applied to non-empty list
+     */
+    public function testFoldlDirectionIndependentNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            15,
+            $listOps->foldl(static fn ($acc, $el) => $acc + $el, [1, 2, 3, 4], 5)
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the left with a function -> direction dependent function applied to non-empty list
+     */
+    public function testFoldlDirectionDependentNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            64,
+            $listOps->foldl(static fn ($acc, $el) => $el / $acc, [1, 2, 3, 4], 24)
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the right with a function -> empty list
+     */
+    public function testFoldrEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            2,
+            $listOps->foldr(static fn ($acc, $el) => $el * $acc, [], 2)
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the right with a function -> direction independent function applied to non-empty list
+     */
+    public function testFoldrDirectionIndependentNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            15,
+            $listOps->foldr(static fn ($acc, $el) => $acc + $el, [1, 2, 3, 4], 5)
+        );
+    }
+
+    /**
+     * @testdox folds (reduces) the given list from the right with a function -> direction dependent function applied to non-empty list
+     */
+    public function testFoldrDirectionDependentNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals(
+            9,
+            $listOps->foldr(static fn ($acc, $el) => $el / $acc, [1, 2, 3, 4], 24)
+        );
+    }
+
+    /**
+     * @testdox reverse the elements of a list -> empty list
+     */
+    public function testReverseEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([], $listOps->reverse([]));
+    }
+
+    /**
+     * @testdox reverse the elements of a list -> non-empty list
+     */
+    public function testReverseNonEmptyList()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([7, 5, 3, 1], $listOps->reverse([1, 3, 5, 7]));
+    }
+
+    /**
+     * @testdox reverse the elements of a list -> list of lists is not flattened
+     */
+    public function testReverseNonEmptyListIsNotFlattened()
+    {
+        $listOps = new ListOps();
+        $this->assertEquals([[4, 5, 6], [], [3], [1, 2]], $listOps->reverse([[1, 2], [3], [], [4, 5, 6]]));
+    }
+}

+ 47 - 0
list-ops/README.md

@@ -0,0 +1,47 @@
+# List Ops
+
+Welcome to List Ops on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Instructions
+
+Implement basic list operations.
+
+In functional languages list operations like `length`, `map`, and `reduce` are very common.
+Implement a series of basic list operations, without using existing functions.
+
+The precise number and names of the operations to be implemented will be track dependent to avoid conflicts with existing names, but the general operations you will implement include:
+
+- `append` (_given two lists, add all items in the second list to the end of the first list_);
+- `concatenate` (_given a series of lists, combine all items in all lists into one flattened list_);
+- `filter` (_given a predicate and a list, return the list of all items for which `predicate(item)` is True_);
+- `length` (_given a list, return the total number of items within it_);
+- `map` (_given a function and a list, return the list of the results of applying `function(item)` on all items_);
+- `foldl` (_given a function, a list, and initial accumulator, fold (reduce) each item into the accumulator from the left_);
+- `foldr` (_given a function, a list, and an initial accumulator, fold (reduce) each item into the accumulator from the right_);
+- `reverse` (_given a list, return a list with all the original items, but in reversed order_).
+
+Note, the ordering in which arguments are passed to the fold functions (`foldl`, `foldr`) is significant.
+
+## Callable
+
+In PHP there is a concept of [callable](https://www.php.net/manual/en/language.types.callable.php).
+
+Those can take multiple forms, but we will focus on [anonymous functions](https://www.php.net/manual/en/functions.anonymous.php).
+
+It is possible to create an anonymous function in a variable and call it with parameters:
+
+```php
+$double = function ($number) {
+    return $number * 2;
+}
+
+$double(2); // returns 4
+$double(4); // returns 8
+```
+
+## Source
+
+### Created by
+
+- @homersimpsons

+ 10 - 0
protein-translation/.exercism/config.json

@@ -0,0 +1,10 @@
+{
+  "blurb": "Translate RNA sequences into proteins.",
+  "authors": ["MichaelBunker"],
+  "contributors": [],
+  "files": {
+    "solution": ["ProteinTranslation.php"],
+    "test": ["ProteinTranslationTest.php"],
+    "example": [".meta/example.php"]
+  }
+}

+ 1 - 0
protein-translation/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"protein-translation","id":"a8eb2e85efd14f0fb3259b51969cd61e","url":"https://exercism.org/tracks/php/exercises/protein-translation","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
protein-translation/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit ProteinTranslation.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 98 - 0
protein-translation/ProteinTranslation.php

@@ -0,0 +1,98 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class ProteinError {
+
+  protected string $msg;
+
+  public function __construct(string $msg) {
+    $this->msg = $msg;
+  }
+
+  public function Error(): string {
+    return $this->msg;
+  }
+
+}
+
+const STOP = "STOP";
+const ErrStop = new ProteinError(STOP);
+const ErrInvalidCodon = new ProteinError("Invalid codon");
+
+class ProteinTranslation {
+
+  public function getProteins(string $rna) {
+    return $this->FromRNA($rna);
+  }
+
+  private function FromRNA(string $rna): array|ProteinError {
+    $names = [];
+    for ($i = 0; $i < strlen($rna); $i += 3) {
+      $codon = substr($rna, $i, 3);
+      $nameOrError = $this->FromCodon($codon);
+      if (is_string($nameOrError)) {
+        $names[] = $nameOrError;
+        continue;
+      }
+      if ($nameOrError == ErrStop) {
+        return $names;
+      }
+      throw new InvalidArgumentException($nameOrError->Error());
+    }
+    return $names;
+  }
+
+  private function FromCodon(string $codon): string|ProteinError {
+    $names = [
+      "AUG" => "Methionine",
+      "UUU" => "Phenylalanine",
+      "UUC" => "Phenylalanine",
+      "UUA" => "Leucine",
+      "UUG" => "Leucine",
+      "UCU" => "Serine",
+      "UCC" => "Serine",
+      "UCA" => "Serine",
+      "UCG" => "Serine",
+      "UAU" => "Tyrosine",
+      "UAC" => "Tyrosine",
+      "UGU" => "Cysteine",
+      "UGC" => "Cysteine",
+      "UGG" => "Tryptophan",
+      "UAA" => STOP,
+      "UAG" => STOP,
+      "UGA" => STOP,
+    ];
+    @$name = $names[$codon];
+    if (empty($name)) {
+      return ErrInvalidCodon;
+    }
+    if ($name == STOP) {
+      return ErrStop;
+    }
+    return $name;
+  }
+
+}

+ 200 - 0
protein-translation/ProteinTranslationTest.php

@@ -0,0 +1,200 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class ProteinTranslationTest extends PHPUnit\Framework\TestCase
+{
+    private ProteinTranslation $translater;
+
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'ProteinTranslation.php';
+    }
+
+    public function setUp(): void
+    {
+        $this->translater = new ProteinTranslation();
+    }
+
+    public function testEmptyRnaSequence(): void
+    {
+        $this->assertEquals([], $this->translater->getProteins(''));
+    }
+
+    public function testMethionineRnaSequence(): void
+    {
+        $this->assertEquals(['Methionine'], $this->translater->getProteins('AUG'));
+    }
+
+    public function testPhenylalanineRnaSequenceOne(): void
+    {
+        $this->assertEquals(['Phenylalanine'], $this->translater->getProteins('UUU'));
+    }
+
+    public function testPhenylalanineRnaSequenceTwo(): void
+    {
+        $this->assertEquals(['Phenylalanine'], $this->translater->getProteins('UUC'));
+    }
+
+    public function testLeucineRnaSequenceOne(): void
+    {
+        $this->assertEquals(['Leucine'], $this->translater->getProteins('UUA'));
+    }
+
+    public function testLeucineRnaSequenceTwo(): void
+    {
+        $this->assertEquals(['Leucine'], $this->translater->getProteins('UUG'));
+    }
+
+    public function testSerineRnaSequenceOne(): void
+    {
+        $this->assertEquals(['Serine'], $this->translater->getProteins('UCU'));
+    }
+
+    public function testSerineRnaSequenceTwo(): void
+    {
+        $this->assertEquals(['Serine'], $this->translater->getProteins('UCC'));
+    }
+
+    public function testSerineRnaSequenceThree(): void
+    {
+        $this->assertEquals(['Serine'], $this->translater->getProteins('UCA'));
+    }
+
+    public function testSerineRnaSequenceFour(): void
+    {
+        $this->assertEquals(['Serine'], $this->translater->getProteins('UCG'));
+    }
+
+    public function testTyrosineRnaSequenceOne(): void
+    {
+        $this->assertEquals(['Tyrosine'], $this->translater->getProteins('UAU'));
+    }
+
+    public function testTyrosineRnaSequenceTwo(): void
+    {
+        $this->assertEquals(['Tyrosine'], $this->translater->getProteins('UAC'));
+    }
+
+    public function testCysteineRnaSequenceOne(): void
+    {
+        $this->assertEquals(['Cysteine'], $this->translater->getProteins('UGU'));
+    }
+
+    public function testCysteineRnaSequenceTwo(): void
+    {
+        $this->assertEquals(['Cysteine'], $this->translater->getProteins('UGC'));
+    }
+
+    public function testTryptophanRnaSequence(): void
+    {
+        $this->assertEquals(['Tryptophan'], $this->translater->getProteins('UGG'));
+    }
+
+    public function testStopCodonRnaSequenceOne(): void
+    {
+        $this->assertEquals([], $this->translater->getProteins('UAA'));
+    }
+
+    public function testStopCodonRnaSequenceTwo(): void
+    {
+        $this->assertEquals([], $this->translater->getProteins('UAG'));
+    }
+
+    public function testStopCodonRnaSequenceThree(): void
+    {
+        $this->assertEquals([], $this->translater->getProteins('UGA'));
+    }
+
+    public function testToCodonsTranslateToProteins(): void
+    {
+        $this->assertEquals(['Phenylalanine', 'Phenylalanine'], $this->translater->getProteins('UUUUUU'));
+    }
+
+    public function testToDifferentCodonsTranslateToProteins(): void
+    {
+        $this->assertEquals(['Leucine', 'Leucine'], $this->translater->getProteins('UUAUUG'));
+    }
+
+    public function testTranslateRnaStrandToCorrectProteinList(): void
+    {
+        $this->assertEquals(
+            ['Methionine', 'Phenylalanine', 'Tryptophan'],
+            $this->translater->getProteins('AUGUUUUGG')
+        );
+    }
+
+    public function testTranslationStopsIfStopCodonAtBeginningOfSequence(): void
+    {
+        $this->assertEquals([], $this->translater->getProteins('UAGUGG'));
+    }
+
+    public function testTranslationStopsIfStopCodonAtEndOfTwoCodonSequence(): void
+    {
+        $this->assertEquals(['Tryptophan'], $this->translater->getProteins('UGGUAG'));
+    }
+
+    public function testTranslationStopsIfStopCodonAtEndOfThreeCodonSequence(): void
+    {
+        $this->assertEquals(['Methionine', 'Phenylalanine'], $this->translater->getProteins('AUGUUUUAA'));
+    }
+
+    public function testTranslationStopsIfStopCodonInMiddleOfThreeCodonSequence(): void
+    {
+        $this->assertEquals(['Tryptophan'], $this->translater->getProteins('UGGUAGUGG'));
+    }
+
+    public function testTranslationStopsIfStopCodonInMiddleOfSixCodonSequence(): void
+    {
+        $this->assertEquals(
+            ['Tryptophan', 'Cysteine', 'Tyrosine'],
+            $this->translater->getProteins('UGGUGUUAUUAAUGGUUU')
+        );
+    }
+
+    public function invalidCodonDataProvider(): array
+    {
+        return [
+            'Non-existing' => ['AAA'],
+            'Unknown' => ['XYZ'],
+            'Incomplete' => ['AUGU'],
+        ];
+    }
+
+    /**
+     * @dataProvider invalidCodonDataProvider
+     */
+    public function testTranslateFailsForInvalidCodons(string $rna): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('Invalid codon');
+        $this->translater->getProteins($rna);
+    }
+
+    public function testTranslatePassesIfStopCodeBeforeIncompleteSequence(): void
+    {
+        $this->assertEquals(['Phenylalanine', 'Phenylalanine'], $this->translater->getProteins('UUCUUCUAAUGGU'));
+    }
+}

+ 53 - 0
protein-translation/README.md

@@ -0,0 +1,53 @@
+# Protein Translation
+
+Welcome to Protein Translation on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Instructions
+
+Translate RNA sequences into proteins.
+
+RNA can be broken into three nucleotide sequences called codons, and then translated to a polypeptide like so:
+
+RNA: `"AUGUUUUCU"` => translates to
+
+Codons: `"AUG", "UUU", "UCU"`
+=> which become a polypeptide with the following sequence =>
+
+Protein: `"Methionine", "Phenylalanine", "Serine"`
+
+There are 64 codons which in turn correspond to 20 amino acids; however, all of the codon sequences and resulting amino acids are not important in this exercise.  If it works for one codon, the program should work for all of them.
+However, feel free to expand the list in the test suite to include them all.
+
+There are also three terminating codons (also known as 'STOP' codons); if any of these codons are encountered (by the ribosome), all translation ends and the protein is terminated.
+
+All subsequent codons after are ignored, like this:
+
+RNA: `"AUGUUUUCUUAAAUG"` =>
+
+Codons: `"AUG", "UUU", "UCU", "UAA", "AUG"` =>
+
+Protein: `"Methionine", "Phenylalanine", "Serine"`
+
+Note the stop codon `"UAA"` terminates the translation and the final methionine is not translated into the protein sequence.
+
+Below are the codons and resulting Amino Acids needed for the exercise.
+
+Codon                 | Protein
+:---                  | :---
+AUG                   | Methionine
+UUU, UUC              | Phenylalanine
+UUA, UUG              | Leucine
+UCU, UCC, UCA, UCG    | Serine
+UAU, UAC              | Tyrosine
+UGU, UGC              | Cysteine
+UGG                   | Tryptophan
+UAA, UAG, UGA         | STOP
+
+Learn more about [protein translation on Wikipedia](http://en.wikipedia.org/wiki/Translation_(biology))
+
+## Source
+
+### Created by
+
+- @MichaelBunker

+ 26 - 0
raindrops/.exercism/config.json

@@ -0,0 +1,26 @@
+{
+  "authors": [],
+  "contributors": [
+    "arueckauer",
+    "dkinzer",
+    "kenden",
+    "kunicmarko20",
+    "kytrinyx",
+    "lafent",
+    "petemcfarlane"
+  ],
+  "files": {
+    "solution": [
+      "Raindrops.php"
+    ],
+    "test": [
+      "RaindropsTest.php"
+    ],
+    "example": [
+      ".meta/example.php"
+    ]
+  },
+  "blurb": "Convert a number to a string, the content of which depends on the number's factors.",
+  "source": "A variation on FizzBuzz, a famous technical interview question that is intended to weed out potential candidates. That question is itself derived from Fizz Buzz, a popular children's game for teaching division.",
+  "source_url": "https://en.wikipedia.org/wiki/Fizz_buzz"
+}

+ 1 - 0
raindrops/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"raindrops","id":"6fee09afa8ae4b5297ff6cff4b729f57","url":"https://exercism.org/tracks/php/exercises/raindrops","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
raindrops/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit Raindrops.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 49 - 0
raindrops/README.md

@@ -0,0 +1,49 @@
+# Raindrops
+
+Welcome to Raindrops on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Introduction
+
+Raindrops is a slightly more complex version of the FizzBuzz challenge, a classic interview question.
+
+## Instructions
+
+Your task is to convert a number into its corresponding raindrop sounds.
+
+If a given number:
+
+- is divisible by 3, add "Pling" to the result.
+- is divisible by 5, add "Plang" to the result.
+- is divisible by 7, add "Plong" to the result.
+- **is not** divisible by 3, 5, or 7, the result should be the number as a string.
+
+## Examples
+
+- 28 is divisible by 7, but not 3 or 5, so the result would be `"Plong"`.
+- 30 is divisible by 3 and 5, but not 7, so the result would be `"PlingPlang"`.
+- 34 is not divisible by 3, 5, or 7, so the result would be `"34"`.
+
+~~~~exercism/note
+A common way to test if one number is evenly divisible by another is to compare the [remainder][remainder] or [modulus][modulo] to zero.
+Most languages provide operators or functions for one (or both) of these.
+
+[remainder]: https://exercism.org/docs/programming/operators/remainder
+[modulo]: https://en.wikipedia.org/wiki/Modulo_operation
+~~~~
+
+## Source
+
+### Contributed to by
+
+- @arueckauer
+- @dkinzer
+- @kenden
+- @kunicmarko20
+- @kytrinyx
+- @lafent
+- @petemcfarlane
+
+### Based on
+
+A variation on FizzBuzz, a famous technical interview question that is intended to weed out potential candidates. That question is itself derived from Fizz Buzz, a popular children's game for teaching division. - https://en.wikipedia.org/wiki/Fizz_buzz

+ 42 - 0
raindrops/Raindrops.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+function raindrops(int $number): string {
+  $res = "";
+  if (!($number % 3)) {
+    $res .= "Pling";
+  }
+  if (!($number % 5)) {
+    $res .= "Plang";
+  }
+  if (!($number % 7)) {
+    $res .= "Plong";
+  }
+  if (!$res) {
+    $res = "$number";
+  }
+  return $res;
+}

+ 113 - 0
raindrops/RaindropsTest.php

@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class RaindropsTest extends PHPUnit\Framework\TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'Raindrops.php';
+    }
+
+    public function test1(): void
+    {
+        $this->assertSame("1", raindrops(1));
+    }
+
+    public function test3(): void
+    {
+        $this->assertSame("Pling", raindrops(3));
+    }
+
+    public function test5(): void
+    {
+        $this->assertSame("Plang", raindrops(5));
+    }
+
+    public function test7(): void
+    {
+        $this->assertSame("Plong", raindrops(7));
+    }
+
+    public function test6(): void
+    {
+        $this->assertSame("Pling", raindrops(6));
+    }
+
+    public function test9(): void
+    {
+        $this->assertSame("Pling", raindrops(9));
+    }
+
+    public function test10(): void
+    {
+        $this->assertSame("Plang", raindrops(10));
+    }
+
+    public function test14(): void
+    {
+        $this->assertSame("Plong", raindrops(14));
+    }
+
+    public function test15(): void
+    {
+        $this->assertSame("PlingPlang", raindrops(15));
+    }
+
+    public function test21(): void
+    {
+        $this->assertSame("PlingPlong", raindrops(21));
+    }
+
+    public function test25(): void
+    {
+        $this->assertSame("Plang", raindrops(25));
+    }
+
+    public function test35(): void
+    {
+        $this->assertSame("PlangPlong", raindrops(35));
+    }
+
+    public function test49(): void
+    {
+        $this->assertSame("Plong", raindrops(49));
+    }
+
+    public function test52(): void
+    {
+        $this->assertSame("52", raindrops(52));
+    }
+
+    public function test105(): void
+    {
+        $this->assertSame("PlingPlangPlong", raindrops(105));
+    }
+
+    public function test12121(): void
+    {
+        $this->assertSame("12121", raindrops(12121));
+    }
+}

+ 19 - 0
reverse-string/.exercism/config.json

@@ -0,0 +1,19 @@
+{
+  "authors": [
+    "MichaelBunker"
+  ],
+  "files": {
+    "solution": [
+      "ReverseString.php"
+    ],
+    "test": [
+      "ReverseStringTest.php"
+    ],
+    "example": [
+      ".meta/example.php"
+    ]
+  },
+  "blurb": "Reverse a given string.",
+  "source": "Introductory challenge to reverse an input string",
+  "source_url": "https://medium.freecodecamp.org/how-to-reverse-a-string-in-javascript-in-3-different-ways-75e4763c68cb"
+}

+ 1 - 0
reverse-string/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"reverse-string","id":"bc57101cde7046519b745297a69fe96b","url":"https://exercism.org/tracks/php/exercises/reverse-string","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
reverse-string/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit ReverseString.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 30 - 0
reverse-string/README.md

@@ -0,0 +1,30 @@
+# Reverse String
+
+Welcome to Reverse String on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Introduction
+
+Reversing strings (reading them from right to left, rather than from left to right) is a surprisingly common task in programming.
+
+For example, in bioinformatics, reversing the sequence of DNA or RNA strings is often important for various analyses, such as finding complementary strands or identifying palindromic sequences that have biological significance.
+
+## Instructions
+
+Your task is to reverse a given string.
+
+Some examples:
+
+- Turn `"stressed"` into `"desserts"`.
+- Turn `"strops"` into `"sports"`.
+- Turn `"racecar"` into `"racecar"`.
+
+## Source
+
+### Created by
+
+- @MichaelBunker
+
+### Based on
+
+Introductory challenge to reverse an input string - https://medium.freecodecamp.org/how-to-reverse-a-string-in-javascript-in-3-different-ways-75e4763c68cb

+ 38 - 0
reverse-string/ReverseString.php

@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+/**
+ * strrev() does not support multibyte inputs, so we need to use multibyte-aware functions.
+ */
+function reverseString(string $text): string
+{
+  $max = mb_strlen($text);
+  $res = "";
+  for ($i = 0; $i < $max; $i++) {
+    $res .= mb_substr($text, $max-$i-1, 1);
+  }
+  return $res;
+}

+ 68 - 0
reverse-string/ReverseStringTest.php

@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class ReverseStringTest extends PHPUnit\Framework\TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'ReverseString.php';
+    }
+
+    public function testEmptyString(): void
+    {
+        $this->assertEquals("", reverseString(""));
+    }
+
+    public function testWord(): void
+    {
+        $this->assertEquals("tobor", reverseString("robot"));
+    }
+
+    public function testCapitalizedWord(): void
+    {
+        $this->assertEquals("nemaR", reverseString("Ramen"));
+    }
+
+    public function testMultibyte(): void
+    {
+        $this->assertEquals("hâché", reverseString("éhcâh"));
+    }
+
+    public function testSentenceWithPunctuation(): void
+    {
+        $this->assertEquals("!yrgnuh m'I", reverseString("I'm hungry!"));
+    }
+
+    public function testPalindrome(): void
+    {
+        $this->assertEquals("racecar", reverseString("racecar"));
+    }
+
+    public function testEvenSizedWord(): void
+    {
+        $this->assertEquals("reward", reverseString("drawer"));
+    }
+}

+ 27 - 0
roman-numerals/.exercism/config.json

@@ -0,0 +1,27 @@
+{
+  "authors": [],
+  "contributors": [
+    "arueckauer",
+    "dkinzer",
+    "Dog",
+    "kunicmarko20",
+    "kytrinyx",
+    "lafent",
+    "petemcfarlane",
+    "zembrowski"
+  ],
+  "files": {
+    "solution": [
+      "RomanNumerals.php"
+    ],
+    "test": [
+      "RomanNumeralsTest.php"
+    ],
+    "example": [
+      ".meta/example.php"
+    ]
+  },
+  "blurb": "Write a function to convert from normal numbers to Roman Numerals.",
+  "source": "The Roman Numeral Kata",
+  "source_url": "https://codingdojo.org/kata/RomanNumerals"
+}

+ 1 - 0
roman-numerals/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"roman-numerals","id":"660fc8e6dba2451f98e67641b8b03cc9","url":"https://exercism.org/tracks/php/exercises/roman-numerals","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 52 - 0
roman-numerals/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit RomanNumerals.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 65 - 0
roman-numerals/README.md

@@ -0,0 +1,65 @@
+# Roman Numerals
+
+Welcome to Roman Numerals on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Instructions
+
+Write a function to convert from normal numbers to Roman Numerals.
+
+The Romans were a clever bunch. They conquered most of Europe and ruled
+it for hundreds of years. They invented concrete and straight roads and
+even bikinis. One thing they never discovered though was the number
+zero. This made writing and dating extensive histories of their exploits
+slightly more challenging, but the system of numbers they came up with
+is still in use today. For example the BBC uses Roman numerals to date
+their programmes.
+
+The Romans wrote numbers using letters - I, V, X, L, C, D, M. (notice
+these letters have lots of straight lines and are hence easy to hack
+into stone tablets).
+
+```text
+ 1  => I
+10  => X
+ 7  => VII
+```
+
+There is no need to be able to convert numbers larger than about 3000.
+(The Romans themselves didn't tend to go any higher)
+
+Wikipedia says: Modern Roman numerals ... are written by expressing each
+digit separately starting with the left most digit and skipping any
+digit with a value of zero.
+
+To see this in practice, consider the example of 1990.
+
+In Roman numerals 1990 is MCMXC:
+
+1000=M
+900=CM
+90=XC
+
+2008 is written as MMVIII:
+
+2000=MM
+8=VIII
+
+See also: https://www.novaroma.org/via_romana/numbers.html
+
+## Source
+
+### Contributed to by
+
+- @arueckauer
+- @dkinzer
+- @Dog
+- @kunicmarko20
+- @kytrinyx
+- @lafent
+- @petemcfarlane
+- @zembrowski
+
+### Based on
+
+The Roman Numeral Kata - https://codingdojo.org/kata/RomanNumerals

+ 64 - 0
roman-numerals/RomanNumerals.php

@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class mapping
+{
+    public int $n;
+    public string $lit;
+
+    public function __construct(int $n, string $lit)
+    {
+        $this->n = $n;
+        $this->lit = $lit;
+    }
+}
+
+function toRoman(int $number): string
+{
+    $literals = [
+        new mapping(10_00, "M"),
+        new mapping(9_00, "CM"),
+        new mapping(5_00, "D"),
+        new mapping(4_00, "CD"),
+        new mapping(1_00, "C"),
+        new mapping(90, "XC"),
+        new mapping(50, "L"),
+        new mapping(40, "XL"),
+        new mapping(10, "X"),
+        new mapping(9, "IX"),
+        new mapping(5, "V"),
+        new mapping(4, "IV"),
+        new mapping(1, "I"),
+    ];
+    $roman = "";
+    foreach ($literals as $mapping) {
+        while ($number >= $mapping->n) {
+            $roman .= $mapping->lit;
+            $number -= $mapping->n;
+        }
+    }
+    return $roman;
+}

+ 138 - 0
roman-numerals/RomanNumeralsTest.php

@@ -0,0 +1,138 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class RomanNumeralsTest extends PHPUnit\Framework\TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        require_once 'RomanNumerals.php';
+    }
+
+    public function test1(): void
+    {
+        $this->assertSame('I', toRoman(1));
+    }
+
+    public function test2(): void
+    {
+        $this->assertSame('II', toRoman(2));
+    }
+
+    public function test3(): void
+    {
+        $this->assertSame('III', toRoman(3));
+    }
+
+    public function test4(): void
+    {
+        $this->assertSame('IV', toRoman(4));
+    }
+
+    public function test5(): void
+    {
+        $this->assertSame('V', toRoman(5));
+    }
+
+    public function test6(): void
+    {
+        $this->assertSame('VI', toRoman(6));
+    }
+
+    public function test9(): void
+    {
+        $this->assertSame('IX', toRoman(9));
+    }
+
+    public function test27(): void
+    {
+        $this->assertSame('XXVII', toRoman(27));
+    }
+
+    public function test48(): void
+    {
+        $this->assertSame('XLVIII', toRoman(48));
+    }
+
+    public function test49(): void
+    {
+        $this->assertSame('XLIX', toRoman(49));
+    }
+
+    public function test59(): void
+    {
+        $this->assertSame('LIX', toRoman(59));
+    }
+
+    public function test93(): void
+    {
+        $this->assertSame('XCIII', toRoman(93));
+    }
+
+    public function test141(): void
+    {
+        $this->assertSame('CXLI', toRoman(141));
+    }
+
+    public function test163(): void
+    {
+        $this->assertSame('CLXIII', toRoman(163));
+    }
+
+    public function test402(): void
+    {
+        $this->assertSame('CDII', toRoman(402));
+    }
+
+    public function test575(): void
+    {
+        $this->assertSame('DLXXV', toRoman(575));
+    }
+
+    public function test911(): void
+    {
+        $this->assertSame('CMXI', toRoman(911));
+    }
+
+    public function test1024(): void
+    {
+        $this->assertSame('MXXIV', toRoman(1024));
+    }
+
+    public function test1998(): void
+    {
+        $this->assertSame('MCMXCVIII', toRoman(1998));
+    }
+
+    public function test2999(): void
+    {
+        $this->assertSame('MMCMXCIX', toRoman(2999));
+    }
+
+    public function test3000(): void
+    {
+        $this->assertSame('MMM', toRoman(3000));
+    }
+}