Skip to content

Instantly share code, notes, and snippets.

@auroraeosrose
Created July 1, 2015 19:07
Show Gist options
  • Save auroraeosrose/2036d1d675a4bd254450 to your computer and use it in GitHub Desktop.
Save auroraeosrose/2036d1d675a4bd254450 to your computer and use it in GitHub Desktop.
Struct
<?php
// Basically a strict object with no methods
class Struct {
public $foo;
public $bar;
}
$struct = new Struct(['foo' => 'something']); // constructor args would get pushed to properties ???
$struct->foo = 'whatever';
$struct->no = 'foo'; // This would throw an exception
// But how woudl you type it? perhaps a special method to get teh definition?
class Struct {
public $foo;
public $bar;
protected function define() {
return ['foo' => 'int',
'bar' => 'mixed'];
}
$struct = new Struct();
$struct->bar = 'hi!'; // ok
$struct->foo = 2; // ok
$struct->foo = 'boo'; // exception
@auroraeosrose
Copy link
Author

@Crell immutability is actually the easy part from extension land :)

@dshafik
Copy link

dshafik commented Jul 1, 2015

Assuming that \Struct is some sort of internal class provided by the extension, we could define a custom struct called MyStruct like so:

class MyStruct extends \Struct {
    public function __construct(int $foo, string $bar) { }
}

// Automatically propagate all args as public properties (similar to hack)

$struct = new MyStruct(2, 'hi!'); // Setting via constructor

// setting properties
$struct->foo = 'boo'; // throws \TypeError

Also, you could use anonymous classes to implement one offs:

$struct = new class(2, 'hi!') extends \Struct {
    public function __construct(int $foo, string $bar) { }
};

$struct->foo = 'boo'; // throws \TypeError

@dshafik
Copy link

dshafik commented Jul 1, 2015

I personally would like to see Shapes from Hack make their way into PHP (as well as custom types :P)

Extending on @Crell's example, you could use return type hints on get() and argument type hints on set() to enforce types (you could, for example, allow for Numeric on set() but always return a float)

I also would prefer to see this be a proper thing, not a bastardized class. Hack uses:

type MyStruct = shape('foo' => int, 'bar' => string);

I wonder it it would be possible to do something similar in PHP?

Also: now we have an AST, is it possible for extensions to extend syntax? Or is that unrelated?

@Crell
Copy link

Crell commented Jul 1, 2015

Skipping the array characters for the "constructor" makes sense. They're unnecessary, I agree.

@auroraeosrose How is immutability the easy part? What would the syntax for it look like?

@auroraeosrose
Copy link
Author

@Crell do you want only some properties immutable? or the entire struct itself immutable - all is really quite easy - just have an ImmutableStruct :) if you only want some parts immutable that's a harder question

Theoretically @dshafik extended syntax is doable but I don't like pain - as in there's no POC for that I'm not entirely sure it IS doable

@Crell
Copy link

Crell commented Jul 1, 2015

The whole thing. But! with an easy way to spawn a new version thereof. Similar to how PSR-7 works, to wit:

$request = new Request();
$request = $request
  ->withHeader('foo', 'bar')
  ->withMethod('GET');

I'm unclear how to get something like that with a pure struct-ish syntax that wouldn't be just reimplementing what we would do on a class now.

@dshafik
Copy link

dshafik commented Jul 1, 2015

@Crell presumably, if you have an ImmutableStruct as well, any change to a property would result in a clone-with-change being returned:

$struct = new class(1, 'Hello') extends  ImmutableStruct {
     public function __construct(int $foo, string $bar) { }
};

$newStruct = $struct->foo++;
var_dump($struct, $newStruct);

$struct = { 'foo' => 1, 'bar' => 'Hello' }
$newStruct = { 'foo' => 2, 'bar' => 'Hello' }

Alternatively, if property assignment can't have a return, then maybe use method syntax:

$struct = new class(1, 'Hello') extends  ImmutableStruct {
     public function __construct(int $foo, string $bar) { }
};

$newStruct = $struct->foo(2);
var_dump($struct, $newStruct);

$struct = { 'foo' => 1, 'bar' => 'Hello' }
$newStruct = { 'foo' => 2, 'bar' => 'Hello' }

@dshafik
Copy link

dshafik commented Jul 2, 2015

I whipped up a userland proof of concept here: http://3v4l.org/dC8Lu

Some things to note: if you don't declare strict types, then the initial values (constructor args) are not strictly typed, but instead coerced.

Because this is smushed into one file for the example, it declares strict types, but in the real world you'd have to declare that in the calling file. Setting properties is always strict.

It would be nice to not need to call it with values, which you could do by setting them to null, but that just lengthens the definition drastically.

Maybe as an internal class, it could enforce strict types on the constructor anyway? Not sure.

Again, I'd much prefer a syntax like hacks shapes, this was just for funsies.

@auroraeosrose
Copy link
Author

I was thinking of the hack shape style stuff for tuples actually @dshafik - let me see what I can whip up in the code...

@Antnee
Copy link

Antnee commented Jul 3, 2015

Any thoughts on my solution? https://gist.github.com/Antnee/237334e1eb937892ad7a

<?php 
/**
 * Ideas for how I'd like to see structs implemented in PHP7
 *
 * In this example, structs are more like lightweight objects that have no 
 * methods, just properties. They can be "instantiated" and all properties will be 
 * null until defined later, as per classes. It is however not possible to set a 
 * value to a property that was not defined in the struct originally. 
 *  
 * Structs can extend other structs, much like classes can. Properties can also 
 * have default values, just as classes can. All properties are public. It is 
 * possible to override previous default property values in the extending struct, 
 * as is done in classes. 
 *  
 * During instantiation, it is possible to set properties in a similar fashion to 
 * defining array keys and values, ie with named arguments. 
 *  
 * Structs are first class. 
 */
namespace antnee;

struct User {
    string $name;
    string $email;
    bool   $active;
    int    $maxLogins;
}

$user = new User;
$user->name = 'Antnee';
$user->email = '[email protected]';
$user->active = true;
$user->maxLogins = 'yes'; // Throw a catchable fatal error - incorrect type
$user->foo = 'bar'; // Throw a catchable fatal error - unknown property

/**
 * Populating a Struct via a _constructor_ looks like a mix of object and array
 */
$user = new User(
    'name' => 'Antnee',
    'email' => '[email protected]',
    'active' => true,
    'maxLogins' => 3,
);

/**
 * Function taking a User struct as an argument
 */
function acceptUserStruct(User $struct): User {
    var_dump($struct);
    return $struct;
}

acceptUserStruct($user);

/**
 * Extending a struct
 */
struct AdminUser extends User {
    int  $maxLogins = -1;
    bool $admin = true;
}

$admin = new AdminUser(
    'name' => 'Antnee',
    'email' => '[email protected]',
    'active' => true
);

echo json_encode($admin);
/* Echoes:
{
    "name": "Antnee",
    "email": "[email protected]",
    "active": true,
    "maxLogins": -1,
    "admin": true
}
*/

/**
 * Pass in _anonymous_ struct as argument
 */
$name = 'Me';
$email = '[email protected]';
$active = true;
$maxLogins = 5;

echo json_encode(
    struct {
        'name'      => $name,
        'email'     => $email,
        'active'    => $active,
        'maxLogins' => $maxLogins,
    }
);

/* Echoes:
{
    "name": "Me",
    "email": "[email protected]",
    "active": true,
    "maxLogins": 5
}
*/

@Ocramius
Copy link

Ocramius commented Jul 5, 2015

Random annoying suggestion: enable type-checks via AOP during dev, rely just on normal public properties behavior on staging/prod.

StrictPhp already allows for that, although the lib is not yet stable.

@Ocramius
Copy link

Ocramius commented Jul 5, 2015

This approach would work with any DTO/VO btw

@Crell
Copy link

Crell commented Jul 6, 2015

@Antnee That's essentially what I was proposing above, although I would go a step further and include Properties.

What I'm not sure about is inheritance and interfaces. Does extending a struct make sense, or should it be mixins-only via traits? Does an interface on a struct even make sense? I'm not sure.

@Antnee
Copy link

Antnee commented Jul 6, 2015

@Crell - I like the idea of property accessors for classes, but I need something much more lightweight. Something where all properties are public and there are no methods attached, or at least as few as possible. Bare minimum magic. Basically, something that I can use huge numbers of (if necessary) in a small script and not have a server come crashing to the ground. Add accessors to classes (at some point, please!) but let's have simple structs.

Traits vs inheritance? Aren't traits pretty much a workaround for the fact that there is no multiple inheritance in PHP and for handling conflicting methods? Personally, I dislike the trait syntax, and since my struct is nothing more than a strict property container, I don't see any benefit from using traits right now. Just allow multiple inheritance instead. Same job, surely?

<?php
struct User {
    string $name;
    string $email;
    bool   $active;
    int    $maxLogins;
}

struct Admin {
    int  $maxLogins = -1;
    bool $admin = true;
}

/**
 * Extends multiple structs
 */
struct AdminUser extends User, Admin {}

The issue here would be that $maxLogins exists in both User and Admin. Both are int types, but the Admin property has a default value. Would suggest that if one has a default and the other doesn't that the default value is applied, but what if both have default values? Probably going to end up with some messy syntax and traits become the preferred solution. I may have just convinced myself that traits are a better solution... but I still don't like the syntax...

@Crell
Copy link

Crell commented Jul 10, 2015

Well, the concern with property access on classes is that determining at runtime what was a property and what was a bare object member was prohibitively costly. My thinking is that if structs are a new thing, baking property accessors in from the get-go sidesteps that question. (And it's on value objects where they would be most useful anyway.) That said, they can/should be optional. If all you want is the default behavior of write-to-property/read-from-property, sure, leave them out.

Traits vs. multiple inheritance: Meh, as long as I can mix multiple pieces together to make one big piece I'm impartial on the details. I'm thinking, eg, cases where I've had Doctrine objects with 4-5 traits in them for fields that are shared between many entities (one property, 2 methods, plus annotations), but inheritance is a wrong/unworkable tool. Traits are fantastic for that, I found. 😄

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