nt( 'saving' ) === false ) { return false; } // update or create. if ( $this->id ) { $saved = $this->isDirty() ? $this->update() : true; } else { $saved = $this->create(); } if ( $this->isError( $saved ) ) { return $saved; } $this->fireModelEvent( 'saved' ); $this->syncOriginal(); return $saved; } /** * Create a new model * * @param array $attributes Attributes to create. * * @return $this|false */ protected function create( $attributes = [] ) { if ( $this->fireModelEvent( 'creating' ) === false ) { return false; } if ( $attributes ) { $this->syncOriginal(); $this->fill( $attributes ); } // add created by WordPress param. $user_id = get_current_user_id(); if ( $user_id && isset( $this->metadata ) ) { $this->metadata->wp_created_by = $user_id; } $created = $this->makeRequest( [ 'method' => 'POST', 'body' => [ $this->object_name => $this->getAttributes(), ], ] ); // bail if error. if ( $this->isError( $created ) ) { return $created; } // reset. $this->resetAttributes(); // fill. $this->fill( $created ?? [] ); // fire event. $this->fireModelEvent( 'created' ); // clear account cache. if ( $this->cachable || $this->clears_account_cache ) { \SureCart::account()->clearCache(); } return $this; } /** * Queue a sync job for later. * * @return \SureCart\Background\QueueService */ protected function queueSync() { return \SureCart::queue()->async( 'surecart/sync/product', [ 'id' => $this->id ] ); } /** * Only return specific properties from the model. * * @param array $attributes Attributes to return. * * @return array */ protected function only( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : func_get_args(); return array_intersect_key( $this->toArray(), array_flip( $attributes ) ); } /** * Return all attributes except the ones passed. * * @param array $attributes Attributes to exclude. */ protected function without( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : func_get_args(); return array_diff_key( $this->toArray(), array_flip( $attributes ) ); } /** * Update the model. * * @param array $attributes Attributes to update. * @return $this|\WP_Error|false */ protected function update( $attributes = [] ) { if ( $this->fireModelEvent( 'updating' ) === false ) { return false; } if ( $attributes ) { $this->syncOriginal(); $this->fill( $attributes ); } $attributes = $this->attributes; unset( $attributes['id'] ); $updated = $this->makeRequest( [ 'id' => $this->id, 'method' => 'PATCH', 'body' => [ $this->object_name => $attributes, ], ] ); if ( $this->isError( $updated ) ) { return $updated; } $this->resetAttributes(); $this->fill( $updated ); $this->fireModelEvent( 'updated' ); // clear account cache. if ( $this->cachable || $this->clears_account_cache ) { \SureCart::account()->clearCache(); } return $this; } /** * Delete the model. * * @param string $id The id of the model to delete. * @return $this|false */ protected function delete( $id = '' ) { $this->id = $id ? $id : $this->id; if ( $this->fireModelEvent( 'deleting' ) === false ) { return false; } $deleted = $this->makeRequest( [ 'id' => $this->id, 'method' => 'DELETE', ] ); if ( $this->isError( $deleted ) ) { return $deleted; } $this->fireModelEvent( 'deleted' ); // clear account cache. if ( $this->cachable || $this->clears_account_cache ) { \SureCart::account()->clearCache(); } return $deleted; } /** * Set a model relation. * * @param string $attribute Attribute name. * @param string $value Value to set. * @param string $model Model name. * @return void */ public function setRelation( $attribute, $value, $model ) { if ( $value ) { $this->attributes[ $attribute ] = is_string( $value ) ? $value : new $model( $value ); } } /** * Get a relation id. * * @param string $attribute Attribute name. * @return string|null; */ public function getRelationId( $attribute ) { $value = $this->attributes[ $attribute ] ?? null; return ! empty( $value['id'] ) ? $value['id'] : $value; } /** * Set a model collection. * * @param string $attribute Attribute name. * @param \SureCart\Models\Collection $collection Collection. * @param string $model Model name. * * @return void */ public function setCollection( $attribute, $collection, $model ) { $models = []; if ( ! empty( $collection->data ) && is_array( $collection->data ) ) { foreach ( $collection->data as $attributes ) { $models[] = is_a( $attributes, $model ) ? $attributes : new $model( $attributes ); } $collection->data = $models; } $this->attributes[ $attribute ] = $collection; } /** * Get the model attributes * * @return array */ public function getAttributes() { return json_decode( wp_json_encode( $this->attributes ), true ); } /** * Get a specific attribute. * * Fires the `surecart/{object_name}/get_attribute` filter, allowing * third-party code to modify the returned value. * * Note: filter callbacks should use $value (first argument) for the * current attribute. Accessing other attributes on $model (e.g. * $model->slug inside a name filter) returns unfiltered values to * prevent recursion. Avoid re-reading the same key — use $value instead. * * @param string $key Attribute name. * * @return mixed */ public function getAttribute( $key ) { $attribute = null; if ( $this->hasAttribute( $key ) ) { $attribute = $this->attributes[ $key ]; } $getter = $this->getMutator( $key, 'get' ); if ( $getter ) { $attribute = $this->{$getter}( $attribute ); } if ( ! $this->is_filtering_attribute && $this->object_name && has_filter( "surecart/{$this->object_name}/get_attribute" ) ) { $this->is_filtering_attribute = true; try { $attribute = apply_filters( "surecart/{$this->object_name}/get_attribute", $attribute, $key, $this ); } finally { $this->is_filtering_attribute = false; } } return $attribute; } /** * Get a in-memory cached attribute. * * @param string $key The attribute key. * @return mixed|null The cached value or null if not found. */ protected function getCachedAttribute( $key ) { if ( ! isset( self::$attribute_cache ) ) { self::$attribute_cache = []; } if ( empty( $this->id ) ) { return null; } return self::$attribute_cache[ $this->id . '_' . $key ] ?? null; } /** * Set a cached attribute in memory. * * @param string $key The attribute key. * @param mixed $value The value to cache. * @return void */ protected function setAttributeCache( $key, $value ) { if ( ! isset( self::$attribute_cache ) ) { self::$attribute_cache = []; } if ( empty( $this->id ) ) { return; } self::$attribute_cache[ $this->id . '_' . $key ] = $value; } /** * Serialize to json. * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->toArray(); } /** * Convert to object. * * @return Object */ public function toObject() { $attributes = (object) $this->attributes; // Check if any accessor is available and call it. foreach ( get_class_methods( $this ) as $method ) { if ( method_exists( get_class(), $method ) ) { continue; } if ( 'get' === substr( $method, 0, 3 ) && 'Attribute' === substr( $method, -9 ) ) { $key = str_replace( [ 'get', 'Attribute' ], '', $method ); if ( $key ) { $pieces = preg_split( '/(?=[A-Z])/', $key ); $pieces = array_map( 'strtolower', array_filter( $pieces ) ); $key = implode( '_', $pieces ); $attributes->$key = $this->getAttribute( $key ); } } } // Check if any attribute is a model and call toArray. array_walk_recursive( $attributes, function ( &$value ) { if ( is_a( $value, Objectable::class ) ) { $value = $value->toObject(); } } ); return $attributes; } /** * Calls accessors during toArray. * * @return array */ public function toArray() { $attributes = $this->getAttributes(); // Check if any accessor is available and call it. foreach ( get_class_methods( $this ) as $method ) { if ( ! method_exists( get_class( $this ), $method ) ) { continue; } if ( 'get' === substr( $method, 0, 3 ) && 'Attribute' === substr( $method, -9 ) ) { $key = str_replace( [ 'get', 'Attribute' ], '', $method ); if ( $key ) { $pieces = preg_split( '/(?=[A-Z])/', $key ); $pieces = array_map( 'strtolower', array_filter( $pieces ) ); $key = implode( '_', $pieces ); $attributes[ $key ] = $this->getAttribute( $key ); } } } // Check if any attribute is a model and call toArray. array_walk_recursive( $attributes, function ( &$value ) { if ( is_a( $value, Arrayable::class ) ) { $value = $value->toArray(); } } ); return array_merge( $attributes, $this->relations ); } /** * Is the model dirty (has unsaved items) * * @param array $attributes Optionally pass attributes to check. * * @return boolean */ public function isDirty( $attributes = null ) { $dirty = $this->getDirty(); if ( is_null( $attributes ) ) { return count( $dirty ) > 0; } if ( ! is_array( $attributes ) ) { $attributes = func_get_args(); } foreach ( $attributes as $attribute ) { if ( array_key_exists( $attribute, $dirty ) ) { return true; } } return false; } /** * Get dirty (unsaved) items * * @return array */ public function getDirty() { $dirty = []; foreach ( $this->attributes as $key => $value ) { if ( ! array_key_exists( $key, $this->original ) ) { $dirty[ $key ] = $value; } elseif ( $value !== $this->original[ $key ] && ! $this->originalIsNumericallyEquivalent( $key ) ) { $dirty[ $key ] = $value; } } return $dirty; } /** * Determine if the new and old values for a given key are numerically equivalent. * * @param string $key Attribute name. * @return bool */ protected function originalIsNumericallyEquivalent( $key ) { $current = $this->attributes[ $key ]; $original = $this->original[ $key ]; return is_numeric( $current ) && is_numeric( $original ) && strcmp( (string) $current, (string) $original ) === 0; } /** * Get the original item * * @param string $key Name of the item. * * @return mixed */ public function getOriginal( $key = null ) { if ( ! is_null( $key ) ) { return isset( $this->original[ $key ] ) ? $this->original[ $key ] : null; } return $this->original; } /** * Get the attribute * * @param string $key Attribute name. * * @return mixed */ public function __get( $key ) { return $this->getAttribute( $key ); } /** * Set the attribute * * @param string $key Attribute name. * @param mixed $value Value of attribute. * * @return void */ public function __set( $key, $value ) { $this->setAttribute( $key, $value ); } /** * Determine if the given attribute exists. * * @param mixed $offset Name. * @return bool */ public function offsetExists( $offset ): bool { return ! is_null( $this->getAttribute( $offset ) ); } /** * Get the value for a given offset. * * @param mixed $offset Name. * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet( $offset ) { return $this->getAttribute( $offset ); } /** * Set the value for a given offset. * * @param mixed $offset Name. * @param mixed $value Value. * @return void */ public function offsetSet( $offset, $value ): void { $this->setAttribute( $offset, $value ); } /** * Unset the value for a given offset. * * @param mixed $offset Name. * @return void */ public function offsetUnset( $offset ): void { unset( $this->attributes[ $offset ], $this->relations[ $offset ] ); } /** * Determine if an attribute or relation exists on the model. * * @param string $key Name. * @return bool */ public function __isset( $key ) { return $this->offsetExists( $key ); } /** * Unset an attribute on the model. * * @param string $key Name. * @return void */ public function __unset( $key ) { $this->offsetUnset( $key ); } /** * Forward call to method * * @param string $method Method to call. * @param mixed $params Method params. */ public function __call( $method, $params ) { return call_user_func_array( [ $this, $method ], $params ); } /** * Static Facade Accessor * * @param string $method Method to call. * @param mixed $params Method params. * * @return mixed */ public static function __callStatic( $method, $params ) { return call_user_func_array( [ new static(), $method ], $params ); } }