Skip to content

Instantly share code, notes, and snippets.

@paragonie-scott
Last active November 18, 2017 07:07
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paragonie-scott/a6d1f3022079871e902a to your computer and use it in GitHub Desktop.
Save paragonie-scott/a6d1f3022079871e902a to your computer and use it in GitHub Desktop.
PHP7 Strict Typing or Else

Let's say you have a file like this:

<?php
declare(strict_types=1);

class Foo
{
    protected $x;
    public function __construct(array $x = [])
    {
        $this->x = $x;
    }
    
    public function bar(int $y): string
    {
        if (!array_key_exists($y, $this->z)) {
            throw new Exception("Key not found");
        }
    }
    
    public function baz(string $y): int
    {
        foreach ($this->x as $key => $value) {
            if ($y === $value) {
                return $key;
            }
        }
        throw new Exception("Value not found");
    }
}

PHP 7 enforces strictness on a per-file basis. So even though you're using them, if someone writes another script that calls it like this, it will work:

<?php
require "src/Foo.php";
$data = new Foo(['a', 'b', 'c', 'def']);
var_dump($data->bar('2')); // string(1) "c"

If you wanted to be completely hostile towards your users and demand they use strict types, here's how you do it:

<?php
declare(strict_types=1);

class Foo
{
    protected $x;
    public function __construct(array $x = [])
    {
        $this->x = $x;
    }
    
    // Notice: No parameter type declarations
    public function bar($y): string
    {
        return $this->actualBar($y);
    }
    // Notice: No parameter type declarations
    public function baz($y): int
    {
        return $this->actualBaz($y);
    }
    
    protected function actualBar(int $y): string
    {
        if (!array_key_exists($y, $this->z)) {
            throw new Exception("Key not found");
        }
    }
    
    protected function actualBaz(string $y): int
    {
        foreach ($this->x as $key => $value) {
            if ($y === $value) {
                return $key;
            }
        }
        throw new Exception("Value not found");
    }

Because the public API doesn't enforce types, PHP won't use weakly-typed conversion rules. It will pass them wholesale.

Then, when your public API methods (which exist in bat country a.k.a. strict_types=1 land) pass them to your protected methods, the strict types rules will be enforced. And thus, TypeError gets thrown, regardless of your user's type preference.

@giritli
Copy link

giritli commented Feb 12, 2016

Why.. you still end up with the types you need in the class regardless.

@paragonie-scott
Copy link
Author

Why.. you still end up with the types you need in the class regardless.

The answer is, "Because I want it to TypeError if they don't pass the correct type to my API."

Why anyone would actually want to do that, I don't know. I work in software security; thinking of ways to make software more malicious is kind of a hobby horse.

Don't judge me.

@Ocramius
Copy link

Why.. you still end up with the types you need in the class regardless.

Strict scalar type checking is done from the consumer perspective. This approach shifts the consumer into your code, which then triggers strict hints anyway.

As a negative side, the reflection of the type hints is lost, as well as interface compatibility (should your interfaces have scalar hints in them), so it is overall a bad solution, IMO.

@paragonie-scott
Copy link
Author

so it is overall a bad solution, IMO

Agreed. Nobody actually implement this, please. It's a joke. Like https://github.com/ircmaxell/PhpGenerics but less thought-out.

@narfbg
Copy link

narfbg commented Feb 12, 2016

It's a bad solution because internals ultimately decided against the good one (enforceable type checks). I was very vocal back then and I still believe 2 separate syntaxes was the way to go, but it's too late now.

@Ocramius
Copy link

@narfbg we could just switch to strict-only for 8 :-P

@paragonie-scott also look at https://github.com/krakjoe/autostrict

@AdamKyle
Copy link

Do not do this.

@narfbg
Copy link

narfbg commented Feb 12, 2016

@Ocramius Right ... and call 8 a 6 instead? ;)

The largest problem is, even if we somehow gain support of dropping strict_types and introducing an explicit syntax for strict checks, it'd be sub-optimal at the very least. function weak((int) $var) vs function strict(int $var) makes the most sense and that ship has sailed now.

@jeremygiberson
Copy link

As a negative side, the reflection of the type hints is lost, as well as interface compatibility (should your interfaces have scalar hints in them), so it is overall a bad solution, IMO.

Change the proffered solution to retain the type hint in the wrapping method. Same thing is accomplished, interface compatibility unaffected.

    public function bar(int $y): string
    {
        return $this->actualBar($y);
    }

    public function baz(string $y): int
    {
        return $this->actualBaz($y);
    }

@narfbg
Copy link

narfbg commented Feb 13, 2016

@jeremygiberson Nope ... that's not the same thing.

It does a cast to the specified type, while the whole point of the gist is to directly reject data of the wrong type.
i.e. bar('1') should not be accepted at all.

@jeremygiberson
Copy link

@narfbg Oh yeah, I totally overlooked that detail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment