Skip to content

Instantly share code, notes, and snippets.

@cspray
Last active May 31, 2022 00:28
Show Gist options
  • Save cspray/0f4e67e8731a8ea9382147707e75287f to your computer and use it in GitHub Desktop.
Save cspray/0f4e67e8731a8ea9382147707e75287f to your computer and use it in GitHub Desktop.
RFC: Introduce a class-string type

RFC: Introduce a class-string type

  • Version: 0.3
  • Last Substantial Updates: 2022-05-04
  • Target Version: PHP 8.next

Introduction

There are many scenarios in which userland code might need to pass parameters to a method or assign a value to a property that is intended to represent a class. Currently the only way to type-hint that a provided value represents a fully-qualified class-name (FQCN) is through userland code. This RFC proposes adding a new internal type that would verify provided values are FQCN.

Motivation

There are several motivations for wanting to include this type and not rely on userland to provide this functionailty:

Improve details communicated about the code

Like all types one of the primary purposes of this is to convey more meaningful information to people reading a given codebase. While it is possible to type-hint string and declare your property or parameter name in such a way that it conveys what is being requested this feels subpar at best. This RFC takes the opionin that information about types should be conveyed through the type system and not the constructs around it.

Throwing errors sooner

Generally speaking, throwing errors and failing fast is preferred. Throwing an error where an invalid type is used could prevent unneccesary instructions from being executed and causes the failure point to be closer to the calling code.

Provide consistent mechanism for common concern

There are currently libraries1 that exist in userland for declaring a type that represents a PHP class. Having a consistent type for representing a class could, albeit slightly, improve the interoperability between codebases. Still, the userland libraries that provide additional features for representing a PHP type could make use of this new type inside their own code.

It is also worth pointing out that unlike other features that could have the same principle applied, for example HTTP abstractions being in userland vs internal, the domain for this RFC is the PHP language itself. As evidenced in the "Use Case" section there's a need for this type currently and this RFC takes the opinion that PHP should provide a suitable construct for describing itself.

Proposal

Other languages, such as Java2, have the concept of a Class class. Adding such a concept to PHP would be very complex, heavy-handed, and would be hard to introduce in ways that provides backwards compatibility with existing codebases. Additionally, existing static analysis tools, such as vimeo/psalm and phpstan/phpstan, have the concept of a class-string which works well with a common way of providing class names, through the Object::class or $instance::class constructs. For these reasons, this RFC proposes adding a new type that will accept string values that are FQCN. With the hyphen present in class-string it likely could not be used for our purposes. With the hyphen removed the type classstring becaomes difficult to read and prone to typos with the 3 sss characters in a row. Other alternatives, in no specific order, could be:

  • classtype
  • classname
  • fqcn
  • classable

For the sake of clarity the rest of this RFC will refer to the type as classsstring until a primary alternative is chosen. At the time of this draft this RFC does not have a strong opinion on which name to choose.

Applicable Uses

The classstring could be type-hinted anywhere the string type is currently allowed. This would include:

  • Class properties
  • Method and function parameters
  • Method and function return types

It is important to note that the classstring type would be available in type unions but not type intersects.

Runtime Changes

At runtime any strings passed to the classstring type that is not a loadable class or interface a TypeError, or other valid Throwable, would be thrown. This check would cause autoloading of the class or interface to trigger if required. This check would occur:

  • When a typed property is assigned a value.
  • When a method or function is invoked with typed parameters.
  • When a method or function with a return type has its return value checked.

Use Cases

I have 2 use cases in personal projects that include the loading of a Plugin architecture3 and the static analysis of a codebase to create a Container4. A highly cursory search for class-string on GitHub returned 1.3m+ results for PHP5. This many uses indicates that there's a large amount of existing code that could potentially utilize this new type.

Examples

Class Property

<?php

namespace Acme;

class GoodPropertyDemo {

    private classstring $type;
    
    private classstring $withDefault = PropertyDemo::class;

    // The type can be nullable
    private ?classstring $nullableType = null;
    
    // The type can be in a union of other types
    private classstring|false|int $someUnion = false;

}

class BadPropertyDemo {

    // Throws an error when this class is instantiated and the property is assigned the default value
    private classstring $type = 'baz';

    // Throws an error that classstring can't be used in an intersect type. 
    // This should occur whenever type intersect validity is checked
    private classstring&GoodPropertyDemo $foo;

}

Method/Function Parameter

<?php

namespace Acme;

class GoodMethodParameter {

    public function register(classstring $service) {
    }
    
    public function withDefault(classstring $service = GoodMethodParameter::class) {
    }
    
    public function withNullable(?classstring $service = null) {
    }
    
    public function inTypeUnion(classstring|int $service = 42) {
    }

}

class BadMethodParameter {

    // An exception would be thrown if this method is invoked with the default parameter values
    // because there is no class or interface 'foobar'
    public function withNotClass(classstring $type = 'foobar') {
    }

    // Throws an error that classstring can't be used in an intersect type. 
    // This should occur whenever type intersect validity is checked
    public function inTypeIntersect(classstring&GoodMethodParameter $param) {
    }

}

Method/Function Return Type

<?php

class GoodReturnType {

    public function getType() : classstring {
        return $this::class;
    }
    
    public function getPossibleType() : ?classstring {
        return null;
    }
    
    public function getTypeOrSomething() : classstring|int {
        return 13;
    }

}

class BadReturnType {

    // Throws an exception when this method is invoked and the return type is checked because it isn't a FQCN
    public function getType() : classstring {
        return '';
    }
    
    // Throws an error that classstring can't be used in an intersect type. 
    // This should occur whenever type intersect validity is checked
    public function getIntersect() : classstring&GoodReturnType {
        // ... 
    }

}

Open Questions

  1. Should classstring|string be allowed?
  2. What are potential performance implications?

Footnotes

Thanks

Ideas are always made better by multiple people providing feedback. This one is no different. Special thanks to the following individuals who provided important guidance in fleshing this RFC out.

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