Asked  7 Months ago    Answers:  5   Viewed   31 times

I have some methods that can return one of two return types. I'm using a framework utilizing MCV so refactoring these few functions in particular is not appealing.

Is it possible to declare the return type returning one or the other and failing on anything else?

function test(): ?
{
    if ($this->condition === false) {
        return FailObject;
    }

    return SucceedObject;
}

 Answers

85

As of PHP 8+, you may use union types:

function test(): FailObject|SuccessObject {}

Another way, available in all versions since PHP 4, is for the two objects to share an interface. Example:

interface ReturnInterface {}
class FailObject implements ReturnInterface {}
class SuccessObject implements ReturnInterface {}
function test(): ReturnInterface {}

In this example, ReturnInterface is empty. Its mere presence supports the needed return type declaration.

You could also use a base, possibly abstract, class.


To me, for this use case, interfaces are more clear and more extensible than union types. For example, if I later want a WarnObject I need only to define it as extending ReturnInterface -- rather than going through all signatures and updating them to FailObject|SuccessObject|WarnObject.

Wednesday, March 31, 2021
 
TuomasR
answered 7 Months ago
50

PHP 7.1 will introduce the iterable typehint which will do exactly that:

function test(iterable $items) { /*...*/ }

See PHP - rfc:iterable.

Until then you can't use any typehint if you want to accept both Traversable and array. The only thing you can do is use a proper @param annotation to document it:

/**
 * @param Traversable|array $items
 */
function test($items) { /*...*/ }
Wednesday, March 31, 2021
 
JohnnyW
answered 7 Months ago
67

I found a ready-to-use solution on Github.

It's called GzipBloat and it does exactly what I was looking for.

First you generate a 10GB gzip file (10MB after first compression) filled with input from /dev/zero

dd if=/dev/zero bs=1M count=10240 | gzip > 10G.gzip

In PHP you then set the content encoding and send the gzip file to the client.

header("Content-Encoding: gzip");
header("Content-Length: ".filesize('10G.gzip'));

//Turn off output buffering
if (ob_get_level()) ob_end_clean();

readfile('10G.gzip');

Results (Win10):

  • IE11: Memory rises, then IE Crashes
  • Chrome 52: Memory rises, error is shown
  • Edge 38: Memory rises, then drops and nothing is displayed (seems to load forever)
  • Nikto: Scans in regular speed, no memory problems
  • SQLmap: High memory then crashes
Friday, May 28, 2021
 
Sabya
answered 5 Months ago
26

Since PHP 7.4 introduces type-hinting for properties, it is particularly important to provide valid values for all properties, so that all properties have values that match their declared types.

A property that has never been assigned doesn't have a null value, but it is on an undefined state, which will never match any declared type. undefined !== null.

For the code above, if you did:

$f = new Foo(1);
$f->getVal();

You would get:

Fatal error: Uncaught Error: Typed property Foo::$val must not be accessed before initialization

Since $val is neither string nor nullwhen accessing it.

The way to get around this is to assign values to all your properties that match the declared types. You can do this either as default values for the property or during construction, depending on your preference and the type of the property.

For example, for the above one could do:

class Foo {

    private int $id;
    private ?string $val = null; // <-- declaring default null value for the property
    private Collection $collection;
    private DateTimeInterface $createdAt;
    private ?DateTimeInterface $updatedAt;

    public function __construct(int $id) {
        // and on the constructor we set the default values for all the other 
        // properties, so now the instance is on a valid state
        $this->id = $id;
        $this->createdAt = new DateTimeImmutable();
        $this->updatedAt = new DateTimeImmutable();

        $this->collection = new ArrayCollection();
    }

Now all properties would have a valid value and the the instance would be on a valid state.

This can hit particularly often when you are relying on values that come from the DB for entity values. E.g. auto-generated IDs, or creation and/or updated values; which often are left as a DB concern.

For auto-generated IDs, the recommended way forward is to change the type declaration to:

private ?int $id = null

For all the rest, just choose an appropriate value for the property's type.

Wednesday, June 9, 2021
 
mattltm
answered 5 Months ago
19

This is getting close to the limits of what I can get out of the type system. TypeScript 4.1 will support recursive conditional types, but even with them I imagine you'll quite possibly get circularity errors, "type instantiation too deep" errors, or other weird errors on anything that tries to use getValue() generically. So I'm not sure I'd actually recommend you use what I'm going to write below:


In another question I wrote how to convince the compiler to give you a union of all the valid key paths of an object, represented as a tuple. It looks like this:

type Cons<H, T> = T extends readonly any[] ? [H, ...T] : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
        P extends [] ? never : Cons<K, P> : never
    ) }[keyof T]
    : [];

You can verify that

type FooPaths = Paths<Foo>;
// type FooPaths = ["a"] | ["b"] | ["c"] | ["c", "lark"] | ["c", "wibble"]

The following definition of DeepIdx<T, KS> will give the type of the property at the key path KS (where KS extends Paths<T> should be true), but in only works like this for TS4.1+:

type DeepIdx<T, KS extends readonly any[]> = KS extends readonly [infer K, ...infer KK] ?
    K extends keyof T ? DeepIdx<T[K], KK> : never : T

You can verify that

type FooCWibble = DeepIdx<Foo, ["c", "wibble"]>;
// type FooCWibble = string

Armed with those, your getValue() can be typed like this without overloads:

function getValue<O, KK extends Paths<O> | []>(
    obj: O, keys: KK
): DeepIdx<O, KK> {
    let out: any = obj;
    for (const k in keys) out = out[k as any]
    return out;
}

You can verify that these work:

const num = getValue(o, ['a']) // number
const str = getValue(o, ['b']) // string 
const boo = getValue(o, ['c', 'lark']) // boolean

getValue(o, ['a', 'b']) // error!
// -------------> ~~~
// b is not assignable to lark | wibble
getValue(o, ['d']) // error!
// --------> ~~~
// d is not assignable to a | b | c

And then this definition also works inside areValuesEqual() if you give keys the type Paths<O>:

function areValuesEqual<O>(obj: O, oldObj: O, keys: Paths<O>) {
    let value = getValue(obj, keys)
    let oldValue = getValue(oldObj, keys)
    return value === oldValue ? true : false
}

For doSomethingAndThenGetValue() you have to make keys generic so the compiler knows what is coming out:

function doSomethingAndThenGetValue<O, K extends Paths<O>>(
  obj: O, 
  oldObj: O, 
  keys: K
): DeepIdx<O, K> {
    let value = getValue(obj, keys)
    console.log("Value obtained is:", value)
    return value
}

I could explain exactly how all of those types work, but it's a bit involved and there's some constructs specifically tailored to coax type inference to work the right way (the | [] hints at a tuple context) or to avoid an immediate circularity warning (the Prev tuple is there to place a governor on the maximum depth of recursion), and I don't know how useful it is to explain in detail something I'd seriously hesitate from putting in any production code base.

For your purposes you might want to just enforce the constraints more at runtime and do something like PropertyKey[] for keys. Inside the implementation of areValuesEqual() or you can just use any[] or a type assertion to make the compiler accept it.


Playground link to code

Wednesday, September 1, 2021
 
RemiX
answered 2 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :