Hacker News new | ask | show | jobs
by NaN1352 1670 days ago
Genuine question from a php hobbyist : what is the equivalent of Typescript’s ability to declare an object’s structure?

It’s really weird to me, I mean don’t we do this all the time? Work with eg. an $options array/obj passedto a constructor, or say, a message decoded from JSON…

I could write $name = $message[‘username’] … and there is no checks in ide or runtime, while the phpdoc will just document $message to be an object or array… what am I missing?

It looks like php devs create full blown classes to represent just about every data strcuture, but what if it’s just data and you don’t need any attached logic’ Isn’t there a concise way to declare a complex type?

3 comments

We do indeed create full blown classes for data objects. However, it is made easier with PHP 8.0 and 8.1 (released today).

Typed properties, constructor properties, and Read-only properties can significantly reduce the code bloat.

- https://php.watch/versions/8.0/constructor-property-promotio...

- https://php.watch/versions/8.1/readonly

Can you directly cast an object / assoc array to an instance of such a class?

If I have a function that receives eg a Message, it means an instance of the class right? So somehow my assoc.array/object still needs to be instanced, and if I do just $message = new Message($theData) … it doesn’t auto assign properties right? That would be handy though still very much boilerplate.

I guessthe language is just designed for how it’s been used so far, may e it will change if JIT makes php more open ended in its uses.

I never had to do this and I don't believe you can do this directly via a language construct but you can do it within a class via a method or trait or static function. This trait can then be applied to any class. Probably be better though to just have a create function that takes an input of type and coverts to class.

Anyway I was curious so I wanted to try this out with the trait method here is working code that illustrates this.

    <?php

    /* trait to cast from any iterable to class */
    /* reusable - close to what I think you are asking for */
    trait castFrom
    {
        static function castFrom($data)
        {
            if (!is_iterable($data) and !is_array($data) and !is_object($data)) return null;
            $class = new self();
            foreach((array)$data as $key => $value)
            {
                if (!property_exists($class, $key)) continue;
                $class->$key = $value;
            }
            return $class;
        }
    }

    class Foo
    {
        use castFrom; // use the castFrom trait
        public string $str = 'blah';
        public int $num = 5;
        public function hello() { echo "{$this->str} : {$this->num}\n"; }
    }

    class Bar
    {
       use castFrom; // we can use the castFrom trait again and again
       public int $x = 0;
       public int $y = 0;
       public function hello() { echo ($this->x * $this->y) . "\n"; }
    }

    $data = ['str' => 'foo', 'num' => 7, 'x' => 3, 'y' => 7];

    $foo = Foo::castFrom($data);
    $foo->hello();

    print_r($foo);

    $obj = (object)$data;

    $bar = Bar::castFrom($obj);
    $bar->hello();

    print_r($bar);

    /* you could just use create functions form the class */
    class Creator
    {
        static public function createFromArray($data)  {} // create from array
        static public function createFromObject($data) {}   // create from object
        static public function createFromSerial($data) {}   // etc..
        static public function createFromJson($data)   {}
    }

EDIT: Added ugly check to make sure the type can be iterated over.
It's not clear to me what you're looking for. But you can cast arrays to objects. And you can use type declarations as arguments for functions.
Ok say I have a small class with an options object (I know perhaps not the best design...):

    class Foo {
      public function __construct(string $name, array $options)
      {
        $this->name = $name;
        if ($options->enableFlag) { ...
I want to declare what $options are, in TS I could do:

type TFooOptions = { enableFlag: boolean; userIds: number[]; }

Then

    function __construct(string $name, TFooOptions $options) ...
Otherwise how do I get proper typechecking if the IDE doesn't know what $options are?
>type TFooOptions

is that creating a JS class under the hood or is just a hint for the compiler?

What I do , but I don't work with recent PHP versions is to create this as a class, then to make this easy to construct objects from JSON i add a static function fromObject or fromJson that does it for me. Using real classes could prevent bugs when working with shit APIs like Microsoft TTS , where they have 2 endponts and one returns objects like voice: {name:"Bob"} and the other voice: {Name:"Bob"} so my code will wrap both type of responses into a well defined class.

I don't think there is a PHPDoc annotation just to hint what that object looks like but I will be happey to be shown a way.

Simple object type in TypeScript, the "type checking" abstraction layer on top of JS, which actually doesn't even need to be compiled (eg. esbuild strips all typing from the code before compiling).

https://www.typescriptlang.org/docs/handbook/2/everyday-type...

Your approach is sensible. I remember using json_encode() indeed so I could declare some data in a php file with <<< HEREDOC in the Javascript short syntax (though short array syntax in php nowadays makes it less painful, JSON syntax still a bit less verbose).

You can get some basic checking, but not type in arrays [1]. Maybe using attributes you could check if it is?

  class TFooOptions
  {
      public function __construct(
          public bool $enableFlag,
          public array $userIds = [],
      ) {
      }
  }

  $options = new TFooOptions(
      enableflags: true,
      userIds: [1,2,3]
  );

  new Foo('Bob', $options);
Or if just for IDE checking, you could use the @property docblock value on the class.

[1] https://wiki.php.net/rfc/arrayof

PHP doesn't do such structural typing, but Psalm[1] can do it statically. It calls them object-like arrays.[2]

[1]: https://psalm.dev/ [2]: https://psalm.dev/docs/annotating_code/type_syntax/array_typ...

You can force a type check that it's a specific type of object or interface rather than an array. But, no, you can't do what you're asking for directly.
The language type system doesn't quite support that yet in all scenarios. As of php 7/8 it's getting there but for what you are describing you'd need to use a static analysis tool still unfortunately.

Here's two of the most common ones. These are very mature and are very broadly used. Most editors also come with some support for these annotations so you often get help in the editor when something is wrong.

https://psalm.dev/docs/annotating_code/supported_annotations...

https://phpstan.org/writing-php-code/phpdoc-types

I feel like TypeScript and Flow went down the structural typing path (rather than relying on nominal types) because they were following what was popular in JavaScript at the time, not because it is an unequivocally better solution. The last time I programmed PHP, I didn't feel having to define lightweight classes was a big deal.

The examples you mentioned are interesting because I consider `options` arguments to be a code smell that tends to happen because TypeScript is missing named parameters, which PHP has. And typing JSON messages is far from a solved problem in TypeScript, because there is no widespread solution for runtime type checking.