Skip to content

Instantly share code, notes, and snippets.

@ohjay666
Forked from loilo/magic-methods.js
Last active November 21, 2023 16:22
Show Gist options
  • Save ohjay666/ba442a4f72e2db2205b0bd111df83b8e to your computer and use it in GitHub Desktop.
Save ohjay666/ba442a4f72e2db2205b0bd111df83b8e to your computer and use it in GitHub Desktop.
PHP Magic Methods in JavaScript

JavaScript Magic Methods

This script implements some of PHP's magic methods for JavaScript classes, using a Proxy.

The fork contains a modification that makes the __get method usable recursively like in php

Example

You can use it like this:

const Foo = magicMethods(class Foo {
  constructor () {
    this.bar = 'Bar'
  }
  
  __get (name) {
    switch (name){
      case 'firstName': return 'Petra';
      case 'lastName': return 'Mustermann';
      // __get also fires within __get
      case 'fullName': return this.firstName + ' ' + this.lastName;
    }
    console.error('undefined prop ' + name);
  }
})

const foo = new Foo
foo.fullName; // Petra Mustermann

If you're using a JavaScript transpiler like Babel with decorators enabled, you can also use the magicMethods function as a decorator:

@magicMethods
class Foo {
  // ...
}

Supported Magic Methods

Given a class Class and an instance of it, the following are the magic methods supported by this script:

__get(name)

Called when trying to access instance[name] where name is not an existing property of instance.

Attention: As in PHP, the check if name exists in instance does not use any custom __isset() methods.

__set(name, value)

Called when trying to do instance[name] = ... where name is neither set as a property of instance.

__isset(name)

Called when trying to check existance of name by calling name in instance.

__unset(name)

Called when trying to unset property name by calling delete instance[name].

Additional Methods

The following magic methods are made available by this script, but are not supported in PHP:

static __getStatic(name)

Like __get(), but in the Class instead of the instance.

static __setStatic(name, value)

Like __set(), but in the Class instead of the instance.

Why is Magic Method X not supported?

They are either not necessary or not practical:

  • __construct() is not needed, there's JavaScript's constructor already.
  • __destruct(): There is no mechanism in JavaScript to hook into object destruction.
  • __call(): Functions are first-class objects in JavaScript. That means that (as opposed to PHP) an object's methods are just regular properties in JavaScript and must first be obtained via __get() to be invoked subsequently. So to implement __call() in JavaScript, you'd simply have to implement __get() and return a function from there.
  • __callStatic(): As in __call(), but with __getStatic().
  • __sleep(), __wakeup(): There's no builtin serialization/unserialization in JavaScript. You could use JSON.stringify()/JSON.parse(), but there's no mechanism to automatically trigger any methods with that.
  • __toString() is already present in JavaScript's toString()
  • __invoke(): JavaScript will throw an error if you'll try to invoke a non-function object, no way to avoid that.
  • __set_state(): There's nothing like var_export() in JavaScript.
  • __clone(): There's no builtin cloning functionality in JavaScript that can be hooked into.
  • __debugInfo(): There's no way to hook into console.log() output.

Can I extend a class with Magic Methods on it?

Yes:

// `Bar` inherits magic methods from `Foo`
class Bar extends Foo {}

Or, if class Bar contains magic methods itself:

const Bar = magicMethods(class Bar extends Foo {
  // You may define `Bar`'s magic methods here
})
function magicMethods (clazz) {
// A toggle switch for the __isset method
// Needed to control "prop in instance" inside of getters
let issetEnabled = true
const classHandler = Object.create(null)
// Trap for class instantiation
classHandler.construct = (target, args, receiver) => {
// Wrapped class instance
const instance = Reflect.construct(target, args, receiver)
// Instance traps
const instanceHandler = Object.create(null)
// __get()
// Catches "instance.property"
const get = Object.getOwnPropertyDescriptor(clazz.prototype, '__get')
if (get) {
instanceHandler.get = (target, name, receiver) => {
// We need to turn off the __isset() trap for the moment to establish compatibility with PHP behaviour
// PHP's __get() method doesn't care about its own __isset() method, so neither should we
issetEnabled = false
const exists = Reflect.has(target, name)
issetEnabled = true
if (exists) {
return Reflect.get(target, name, receiver)
} else {
return get.value.call(instanceProxy, name)
}
}
}
// __set()
// Catches "instance.property = ..."
const set = Object.getOwnPropertyDescriptor(clazz.prototype, '__set')
if (set) {
instanceHandler.set = (target, name, value, receiver) => {
if (name in target) {
Reflect.set(target, name, value, receiver)
} else {
return target.__set.call(target, name, value)
}
}
}
// __isset()
// Catches "'property' in instance"
const isset = Object.getOwnPropertyDescriptor(clazz.prototype, '__isset')
if (isset) {
instanceHandler.has = (target, name) => {
if (!issetEnabled) return Reflect.has(target, name)
return isset.value.call(target, name)
}
}
// __unset()
// Catches "delete instance.property"
const unset = Object.getOwnPropertyDescriptor(clazz.prototype, '__unset')
if (unset) {
instanceHandler.deleteProperty = (target, name) => {
return unset.value.call(target, name)
}
}
// keep instanceProxy to be used in get trap when __get method is called
const instanceProxy = new Proxy(instance, instanceHandler)
return instanceProxy
}
// __getStatic()
// Catches "class.property"
if (Object.getOwnPropertyDescriptor(clazz, '__getStatic')) {
classHandler.get = (target, name, receiver) => {
if (name in target) {
return target[name]
} else {
return target.__getStatic.call(receiver, name)
}
}
}
// __setStatic()
// Catches "class.property = ..."
if (Object.getOwnPropertyDescriptor(clazz, '__setStatic')) {
classHandler.set = (target, name, value, receiver) => {
if (name in target) {
return target[name]
} else {
return target.__setStatic.call(receiver, name, value)
}
}
}
return new Proxy(clazz, classHandler)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment