Asked  7 Months ago    Answers:  5   Viewed   35 times

I have a method which accepts a PDO object as an argument, to allow the user to use an existing connection rather then the method to open a new one, and save resources:

public static function databaseConnect($pdo = null) {

I am aware of is_object() to check if the argument is an object, but I want to check if $pdo is a PDO object, and not just an object.

Because the user can easily enter (by mistake?) a different kind of object, a mysqli or such, and the entire script will break apart.

In short: How can I check a variable for a specific type of object?

 Answers

54

You can use instanceof:

if ($pdo instanceof PDO) {
    // it's PDO
}

Be aware though, you can't negate like !instanceof, so you'd instead do:

if (!($pdo instanceof PDO)) {
    // it's not PDO
}

Also, looking over your question, you can use object type-hinting, which helps enforce requirements, as well as simplify your check logic:

function connect(PDO $pdo = null)
{
    if (null !== $pdo) {
        // it's PDO since it can only be
        // NULL or a PDO object (or a sub-type of PDO)
    }
}

connect(new SomeClass()); // fatal error, if SomeClass doesn't extend PDO

Typed arguments can be required or optional:

// required, only PDO (and sub-types) are valid
function connect(PDO $pdo) { }

// optional, only PDO (and sub-types) and 
// NULL (can be omitted) are valid
function connect(PDO $pdo = null) { }

Untyped arguments allow for flexibility through explicit conditions:

// accepts any argument, checks for PDO in body
function connect($pdo)
{
    if ($pdo instanceof PDO) {
        // ...
    }
}

// accepts any argument, checks for non-PDO in body
function connect($pdo)
{
    if (!($pdo instanceof PDO)) {
        // ...
    }
}

// accepts any argument, checks for method existance
function connect($pdo)
{
    if (method_exists($pdo, 'query')) {
        // ...
    }
}

As for the latter (using method_exists), I'm a bit mixed in my opinion. People coming from Ruby would find it familiar to respond_to?, for better or for worse. I'd personally write an interface and perform a normal type-hint against that:

interface QueryableInterface
{ 
    function query();
}

class MyPDO extends PDO implements QueryableInterface { }

function connect(QueryableInterface $queryable) { }

However, that's not always feasible; in this example, PDO objects are not valid parameters as the base type doesn't implement QueryableInterface.

It's also worth mentioning that values have types, not variables, in PHP. This is important because null will fail an instanceof check.

$object = new Object();
$object = null;
if ($object instanceof Object) {
    // never run because $object is simply null
}

The value loses it's type when it becomes null, a lack of type.

Wednesday, March 31, 2021
 
TMichel
answered 7 Months ago
17

The constructor should be __construct() with two underscores.

http://php.net/manual/en/language.oop5.decon.php

And it will output "Constructor called with parameter one" in your code.

Saturday, May 29, 2021
 
HamidR
answered 5 Months ago
88

Don't check types, check actions. You don't actually care if it's a function (it might be a class instance with a __call__ method, for example) - you just care if it can be called. So use callable(f).

Friday, July 30, 2021
 
madphp
answered 3 Months ago
72

You might check out the substr function in php and grab the first character that way:

http://php.net/manual/en/function.substr.php

if (substr('_abcdef', 0, 1) === '_') { ... }
Sunday, August 8, 2021
 
Shoarc
answered 3 Months ago
44

Problem with method signature merging for union of arrays

The reason TypeScript complains is because with a type of IShop[] | IHotel[] it will merge all method signatures. In particular the signatures:

Array<IShop>.find(
    predicate: (
        value: IShop, 
        index: number, 
        obj: IShop[]
    ) => unknown, thisArg?: any
): IShop | undefined

Array<IHotel>.find(
    predicate: (
        value: IHotel, 
        index: number, 
        obj: IHotel[]
    ) => unknown, thisArg?: any
): IHotel | undefined

Effectively becomes something similar to:

Array<IShop & IHotel>.find(
    predicate: (
        value: IShop & IHotel, 
        index: number,
        obj: (IShop & IHotel)[]
    ) => unknown, thisArg?: any
): IShop & IHotel | undefined

This means that in order to call it, the callback should accept an item that's both IShop and IHotel at the same time and also will produce both an IShop and IHotel at the same time.

That's not actually possible, thus the compiler concludes that as the type signature is unsatisfiable, it is also uncallable.

This is a bit of a weakness in the way of how the method signatures are merged. It is the correct way to merge the signatures but for many use cases, the resulting types are not what you actually need, nor is the method call unsatisfiable. It's more limited in what can satisfy it but definitely not impossible:

let shops = [{name: "shop1", id: 1, type: "supermarket"}];
let hotels = [{name: "hotel1", id: 2, rooms: 42}];

// see addendum
const allRegions = shops.length > 0 ? shops : hotels;

const result = allRegions.find(r => r.name === 'shop1');

console.log(result);

The issue is that this serves a more localised case and not the more general case where for any variation of calling the method.

The way to go around it is to use explicit typing which will allow you to retain type safety but you have to slightly override the compiler's decision.

Possible solutions

Change from a union of arrays, to an array of union type

Since IShop[] | IHotel[] (an array of IShop or array of IHotel) causes method signature merges that is uncallable, we can change the type to (IShop | IHotel)[] (an array of IShop and IHotel items). This is slightly incorrect, as you don't have a mixed array. However, there is almost no difference in practice. You still need to know what each item is, so it's very similar to having an array of either type.

What makes it work is that IShop | IHotel will allow you to use the shared properties between the two interfaces. In this case, name and id. Therefore, TypeScript will allow the call like allRegions.find(r => r.name === 'name').

const allRegions: (IShop | IHotel)[]  = shops.length > 0 ? shops : hotels;

allRegions.find(r => r.name === 'name'); //allowed

Playground Link

Introduce a super type

Very similar to the above but you'd need to change your types:

interface IDataItem {
  name: string,
  id:   number,
}

export interface IShop extends DataItem {
  type: string,   
}

export interface IHotel extends IDataItem {
  rooms: number,   
}

This is extracting the shared properties to an interface and then both IShop and IHotel extend it. This way you can more directly say that allRegions will contain the supertype. The result is essentially the same as the union type IShop | IHotel but made more explicit.

const allRegions: IDataItem[]  = shops.length > 0 ? shops : hotels;

allRegions.find(r => r.name === 'name'); //allowed

Playground Link

If your data is actually related it might be preferable to represent that in your types. The type union does not convey the information about the relation. However, this still requires you to be able to change the types. If that's not a possibility, then a type union is the better option.

Create a new union that will ensure usable array methods

As a brilliant suggestion in a comment from Linda Paiste:

it's possible to declare const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[] so that we get the union signature without losing the restriction that the array elements are of the same type.

Which will give you this:

const allRegions: (IShop[] | IHotel[]) & (IShop | IHotel)[] = shops.length > 0 ? shops : hotels;

allRegions.find(r => r.name === 'name'); //allowed

Playground Link

This is an intersection between two homogenous arrays and a mixed array.

This declaration resolves to (IShop[] & (IShop | IHotel)[]) | (IHotel[] & (IShop | IHotel)[]) which is a union of

  • homogenous IShop array intersected with a mixed IShop | IHotel array
  • homogenous IHotel array intersected with a mixed IShop | IHotel array

The brilliant part is that it behaves exactly the same as IShop[] | IHotel[] - you cannot have a mix. However, at the same time, the type will ensure the method declaration merge works correctly. This means that you get correct type checks for arrays that only have one type of item in them but not mixed:

declare let shops: IShop[];
declare let hotels: IHotel[];
//mixed array
declare let mixed: (IShop | IHotel)[];
//homogenous array of either type
declare let improved: (IShop[] | IHotel[]) & (IShop | IHotel)[];

//something that takes a homogenous array
declare function foo(x: IShop[] | IHotel[]): void;

foo(shops);    //ok
foo(hotels);   //ok
foo(mixed);    //error
foo(improved); //ok

Playground Link

Addendum: clarifying with allRegions initialisation

The line const allRegions = shops.length > 0 ? shops : (hotels.length > 0 ? hotels : []) is superfluous. You only assign an empty array to allRegions is hotels is an empty array (and shops too). Since an empty array is an empty array either case, you can shorten this to const allRegions = shops.length > 0 ? shops : hotels - if hotels is empty, you an empty array anyway. This is what I've used in the code samples above as it makes the code a lot easier to read.

It has the exact same effect as long as you don't plan on mutating the array in-place. That might modify the wrong array.

Saturday, August 28, 2021
 
Precastic
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 :