# isset-php

[![Pipeline Status](https://gitlab.com/enom/isset-php/badges/master/pipeline.svg)](https://gitlab.com/enom/isset-php/pipelines/latest)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)

Safe and simple implementation of PHP `isset()` for JavaScript.

## Installation

This is a zero dependency library that is safe from both *ReferenceError: is not defined* and *TypeError: cannot read property of undefined* exceptions.

```sh
npm install --save isset-php
```

## Description

[TypeScript](https://www.typescriptlang.org/) function signature:

```ts
isset(accessor: Function, ...accessors: Function[]): boolean;
```

Determine if a variable is considered set, this means if a variable is declared and is different than `null` or `undefined`.

If a variable has been unset with the `delete` keyword, it is no longer considered to be set.

`isset()` will return `false` when checking a variable that has been assigned to `null` or `undefined`. Also note that a null character ("\0") is not equivalent to the JavaScript `null` constant.

If multiple parameters are supplied then `isset()` will return `true` only if all of the parameters are considered set. Evaluation goes from left to right and stops as soon as an unset variable is encountered.

## Parameters

**accessor**  
Accessor function returning the variable to be checked.

**accessors**  
Further accessor functions.

## Return Values

Returns `true` if all accessors return a value that is not `null` or `undefined`. `false` otherwise.

## Examples

### Example #1 isset() Examples

```js
const isset = require('isset-php')
let val = ''

// This will evaluate to true so the text will be printed.
if (isset(() => val)) {
  console.log('This val is set so I will print.')
}

// Less compact but still viable except when trying to use `this` context
if (isset(function () { return val })) {
  console.log('This val is set so I will also print.')
}
```

In the next examples we'll use `console.log()` to output the return value of `isset()`.

```js
let a = 'test'
let b = 'anothertest'

console.log(isset(() => a))            // true
console.log(isset(() => a, () => b))   // true

// This might throw a parsing error: Deleting local variable in strict mode
delete a

console.log(isset(() => a))            // false
console.log(isset(() => a, () => b))   // false

let foo = null

console.log(isset(() => foo))          // false
```

This also work for elements in objects:

```js
let a = { test: 1, hello: null, pie: { a: 'apple' } }

console.log(isset(() => a.test))       // true
console.log(isset(() => a.foo))        // false
console.log(isset(() => a.hello))      // false

// The key 'hello' equals null so is considered unset. If you want to check for
// null key values then try:
console.log(Object.prototype.hasOwnProperty.call(a, 'hello')) // true

// Checking deeper object values
console.log(isset(() => a.pie.a))      // true
console.log(isset(() => a.pie.b))      // false
console.log(isset(() => a.cake.a.b))   // false
```

### Example #2 isset() on String Offsets

```js
const expectedArrayGotString = 'somestring'

console.log(isset(() => expectedArrayGotString.some_key))
console.log(isset(() => expectedArrayGotString[0]))
console.log(isset(() => expectedArrayGotString['0']))
console.log(isset(() => expectedArrayGotString[0.5]))
console.log(isset(() => expectedArrayGotString['0.5']))
console.log(isset(() => expectedArrayGotString['0 Mostel']))
```

Output of the above example in PHP 5.4 and above:

```
false
true
true
true  <-- PHP casts 0.5 to 0 for string offsets and throws a notice
false
false
```

Output of the above example in JavaScript:

```
false
true
true
false <-- JS does not cast 0.5 to 0
false
false
```

This is the only caveat with the JavaScript port as this behaviour is not emulated.

---

# Explanation

Pulled from [StackOverflow](https://stackoverflow.com/questions/2281633/javascript-isset-equivalent/46256973#46256973).

## PHP

Note that in PHP you can reference any variable at any depth - even trying to access a non-array as an array will return a simple `true` or `false`:

```js
// Referencing an undeclared variable
isset($some); // false

$some = 'hello';

// Declared but has no depth(not an array)
isset($some); // true
isset($some['nested']); // false

$some = ['nested' => 'hello'];

// Declared as an array but not with the depth we're testing for
isset($some['nested']); // true
isset($some['nested']['deeper']); // false
```

## JavaScript

In JavaScript, we don't have that freedom, we'll always get an error if we do the same  because JS is immediately attempting to access the value of `deeper` before we can wrap it in our `isset()` function so...

```js
// Common pitfall answer(ES6 arrow function)
const isset = (ref) => typeof ref !== 'undefined'

// Same as above
function isset (ref) { return typeof ref !== 'undefined' }

// Referencing an undeclared variable will throw an error, so no luck here
isset(some) // Error: some is not defined

// Defining a simple object with no properties - so we aren't defining
// the property `nested`
let some = {}

// Simple checking if we have a declared variable
isset(some) // true

// Now trying to see if we have a top level property, still valid
isset(some.nested) // false

// But here is where things fall apart: trying to access a deep property
// of a complex object; it will throw an error
isset(some.nested.deeper) // Error: Cannot read property 'deeper' of undefined
//         ^^^^^^ undefined
```

More failing alternatives:

```js
// Any way we attempt to access the `deeper` property of `nested` will
// throw an error
some.nested.deeper.hasOwnProperty('value') // Error
//   ^^^^^^ undefined

// Similar to the above but safe from objects overriding `hasOwnProperty`
Object.prototype.hasOwnProperty.call(some.nested.deeper, 'value') // Error
//                                        ^^^^^^ undefined

// Same goes for typeof
typeof some.nested.deeper !== 'undefined' // Error
//          ^^^^^^ undefined
```

And some working alternatives that can get redundant fast:

```js
// Wrap everything in try...catch
try {
  if (isset(some.nested.deeper)) {
    // ...
  }
} catch (e) {}

try {
  if (some.nested.deeper !== undefined && some.nested.deeper !== null) {
    // ...
  }
} catch (e) {}

// Or by chaining all of the isset which can get long
isset(some) && isset(some.nested) && isset(some.nested.deeper) // false
//                        ^^^^^^ returns false so the next isset() is never run
```
