} /** * Returns the raw data wrapped by this ORM instance as an associative array. Column names may optionally be * supplied as arguments, if so, only those keys will be returned. * * @return array Associative array of the raw data. */ public function as_array() { if ( \func_num_args() === 0 ) { return $this->data; } $args = \func_get_args(); return \array_intersect_key( $this->data, \array_flip( $args ) ); } /** * Returns the value of a property of this object (database row) or null if not present. * * If a column-names array is passed, it will return a associative array with the value of each column or null if * it is not present. * * @param string|array $key Key. * * @return array|mixed|null */ public function get( $key ) { if ( \is_array( $key ) ) { $result = []; foreach ( $key as $column ) { $result[ $column ] = isset( $this->data[ $column ] ) ? $this->data[ $column ] : null; } return $result; } else { return isset( $this->data[ $key ] ) ? $this->data[ $key ] : null; } } /** * Returns the name of the column in the database table which contains the primary key ID of the row. * * @return string The primary key ID of the row. */ protected function get_id_column_name() { if ( ! \is_null( $this->instance_id_column ) ) { return $this->instance_id_column; } return 'id'; } /** * Gets the primary key ID of this object. * * @param bool $disallow_null Whether to allow null IDs. * * @return array|mixed|null * * @throws \Exception Primary key ID contains null value(s). * @throws \Exception Primary key ID missing from row or is null. */ public function id( $disallow_null = false ) { $id = $this->get( $this->get_id_column_name() ); if ( $disallow_null ) { if ( \is_array( $id ) ) { foreach ( $id as $id_part ) { if ( $id_part === null ) { throw new \Exception( 'Primary key ID contains null value(s)' ); } } } else { if ( $id === null ) { throw new \Exception( 'Primary key ID missing from row or is null' ); } } } return $id; } /** * Sets a property to a particular value on this object. * * To set multiple properties at once, pass an associative array as the first parameter and leave out the second * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called. * * @param string|array $key Key. * @param string|null $value Value. * * @return ORM */ public function set( $key, $value = null ) { return $this->set_orm_property( $key, $value ); } /** * Set a property to a particular value on this object as expression. * * To set multiple properties at once, pass an associative array as the first parameter and leave out the second * parameter. Flags the properties as 'dirty' so they will be saved to the database when save() is called. * * @param string|array $key Key. * @param string|null $value Value. * * @return ORM */ public function set_expr( $key, $value = null ) { return $this->set_orm_property( $key, $value, true ); } /** * Sets a property on the ORM object. * * @param string|array $key Key. * @param string|null $value Value. * @param bool $expr Expression. * * @return ORM */ protected function set_orm_property( $key, $value = null, $expr = false ) { if ( ! \is_array( $key ) ) { $key = [ $key => $value ]; } foreach ( $key as $field => $value ) { $this->data[ $field ] = $value; $this->dirty_fields[ $field ] = $value; if ( $expr === false && isset( $this->expr_fields[ $field ] ) ) { unset( $this->expr_fields[ $field ] ); } else { if ( $expr === true ) { $this->expr_fields[ $field ] = true; } } } return $this; } /** * Checks whether the given field has been changed since this object was saved. * * @param mixed $key Key. * * @return bool */ public function is_dirty( $key ) { return \array_key_exists( $key, $this->dirty_fields ); } /** * Checks whether the model was the result of a call to create() or not. * * @return bool */ public function is_new() { return $this->is_new; } /** * Saves any fields which have been modified on this object to the database. * * @return bool True on success. * * @throws \Exception Primary key ID contains null value(s). * @throws \Exception Primary key ID missing from row or is null. */ public function save() { global $wpdb; // Remove any expression fields as they are already baked into the query. $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) ); if ( ! $this->is_new ) { // UPDATE. // If there are no dirty values, do nothing. if ( empty( $values ) && empty( $this->expr_fields ) ) { return true; } $query = \implode( ' ', [ $this->build_update(), $this->add_id_column_conditions() ] ); $id = $this->id( true ); if ( \is_array( $id ) ) { $values = \array_merge( $values, \array_values( $id ) ); } else { $values[] = $id; } } else { // INSERT. $query = $this->build_insert(); } $success = self::execute( $query, $values ); // If we've just inserted a new record, set the ID of this object. if ( $this->is_new ) { $this->is_new = false; if ( $this->count_null_id_columns() !== 0 ) { $column = $this->get_id_column_name(); // If the primary key is compound, assign the last inserted id to the first column. if ( \is_array( $column ) ) { $column = \reset( $column ); } // Explicitly cast to int to make dealing with Id's simpler. $this->data[ $column ] = (int) $wpdb->insert_id; } } $this->dirty_fields = []; $this->expr_fields = []; return $success; } /** * Extracts and gathers all dirty column names from the given model instances. * * @param array $models Array of model instances to be inserted. * * @return array The distinct set of columns that are dirty in at least one of the models. * * @throws \InvalidArgumentException Instance to be inserted is not a new one. */ public function get_dirty_column_names( $models ) { $dirty_column_names = []; foreach ( $models as $model ) { if ( ! $model->orm->is_new() ) { throw new \InvalidArgumentException( 'Instance to be inserted is not a new one' ); } // Remove any expression fields as they are already baked into the query. $dirty_fields = \array_diff_key( $model->orm->dirty_fields, $model->orm->expr_fields ); $dirty_column_names = \array_merge( $dirty_column_names, $dirty_fields ); } $dirty_column_names = \array_keys( $dirty_column_names ); return $dirty_column_names; } /** * Inserts multiple rows in a single query. Expects new rows as it's a strictly insert function, not an update one. * * @example From the Indexable_Link_Builder class: $this->seo_links_repository->query()->insert_many( $links ); * * @param array $models Array of model instances to be inserted. * * @return bool True for successful insert, false for failed. * * @throws \InvalidArgumentException Invalid instances to be inserted. * @throws \InvalidArgumentException Instance to be inserted is not a new one. */ public function insert_many( $models ) { // Validate the input first. if ( ! \is_array( $models ) ) { throw new \InvalidArgumentException( 'Invalid instances to be inserted' ); } if ( empty( $models ) ) { return true; } $success = true; /** * Filter: 'wpseo_chunk_bulked_insert_queries' - Allow filtering the chunk size of each bulked INSERT query. * * @api int The chunk size of the bulked INSERT queries. */ $chunk = \apply_filters( 'wpseo_chunk_bulk_insert_queries', 100 ); $chunk = ! \is_int( $chunk ) ? 100 : $chunk; $chunk = ( $chunk <= 0 ) ? 100 : $chunk; $chunked_models = \array_chunk( $models, $chunk ); foreach ( $chunked_models as $models_chunk ) { $values = []; // First, we'll gather all the dirty fields throughout the models to be inserted. $dirty_column_names = $this->get_dirty_column_names( $models_chunk ); // Now, we're creating all dirty fields throughout the models and // setting them to null if they don't exist in each model. foreach ( $models_chunk as $model ) { $model_values = []; foreach ( $dirty_column_names as $dirty_column ) { // Set the value to null if it hasn't been set already. if ( ! isset( $model->orm->dirty_fields[ $dirty_column ] ) ) { $model->orm->dirty_fields[ $dirty_column ] = null; } // Only register the value if it is not null. if ( ! is_null( $model->orm->dirty_fields[ $dirty_column ] ) ) { $model_values[] = $model->orm->dirty_fields[ $dirty_column ]; } } $values = \array_merge( $values, $model_values ); } // We now have the same set of dirty columns in all our models and also gathered all values. $query = $this->build_insert_many( $models_chunk, $dirty_column_names ); $success = $success && (bool) self::execute( $query, $values ); } return $success; } /** * Updates many records in the database. * * @return int|bool The number of rows changed if the query was succesful. False otherwise. */ public function update_many() { // Remove any expression fields as they are already baked into the query. $values = \array_values( \array_diff_key( $this->dirty_fields, $this->expr_fields ) ); // UPDATE. // If there are no dirty values, do nothing. if ( empty( $values ) && empty( $this->expr_fields ) ) { return true; } $query = $this->join_if_not_empty( ' ', [ $this->build_update(), $this->build_where() ] ); $success = self::execute( $query, \array_merge( $values, $this->values ) ); $this->dirty_fields = []; $this->expr_fields = []; return $success; } /** * Adds a WHERE clause for every column that belongs to the primary key. * * @return string The where part of the query. */ public function add_id_column_conditions() { $query = []; $query[] = 'WHERE'; $keys = \is_array( $this->get_id_column_name() ) ? $this->get_id_column_name() : [ $this->get_id_column_name() ]; $first = true; foreach ( $keys as $key ) { if ( $first ) { $first = false; } else { $query[] = 'AND'; } $query[] = $this->quote_identifier( $key ); $query[] = '= %s'; } return \implode( ' ', $query ); } /** * Builds an UPDATE query. * * @return string The update query. */ protected function build_update() { $query = []; $query[] = "UPDATE {$this->quote_identifier($this->table_name)} SET"; $field_list = []; foreach ( $this->dirty_fields as $key => $value ) { if ( ! \array_key_exists( $key, $this->expr_fields ) ) { $value = ( $value === null ) ? 'NULL' : '%s'; } $field_list[] = "{$this->quote_identifier($key)} = {$value}"; } $query[] = \implode( ', ', $field_list ); return \implode( ' ', $query ); } /** * Builds an INSERT query. * * @return string The insert query. */ protected function build_insert() { $query = []; $query[] = 'INSERT INTO'; $query[] = $this->quote_identifier( $this->table_name ); $field_list = \array_map( [ $this, 'quote_identifier' ], \array_keys( $this->dirty_fields ) ); $query[] = '(' . \implode( ', ', $field_list ) . ')'; $query[] = 'VALUES'; $placeholders = $this->create_placeholders( $this->dirty_fields ); $query[] = "({$placeholders})"; return \implode( ' ', $query ); } /** * Builds a bulk INSERT query. * * @param array $models Array of model instances to be inserted. * @param array $dirty_column_names Array of dirty fields to be used in INSERT. * * @return string The insert query. */ protected function build_insert_many( $models, $dirty_column_names ) { $example_model = $models[0]; $total_placeholders = ''; $query = []; $query[] = 'INSERT INTO'; $query[] = $this->quote_identifier( $example_model->orm->table_name ); $field_list = \array_map( [ $this, 'quote_identifier' ], $dirty_column_names ); $query[] = '(' . \implode( ', ', $field_list ) . ')'; $query[] = 'VALUES'; // We assign placeholders per model for dirty fields that have values and NULL for dirty fields that don't. foreach ( $models as $model ) { $placeholder = []; foreach ( $dirty_column_names as $dirty_field ) { $placeholder[] = ( $model->orm->dirty_fields[ $dirty_field ] === null ) ? 'NULL' : '%s'; } $placeholders = \implode( ', ', $placeholder ); $total_placeholders .= "({$placeholders}),"; } $query[] = \rtrim( $total_placeholders, ',' ); return \implode( ' ', $query ); } /** * Deletes this record from the database. * * @return string The delete query. * * @throws \Exception Primary key ID contains null value(s). * @throws \Exception Primary key ID missing from row or is null. */ public function delete() { $query = [ 'DELETE FROM', $this->quote_identifier( $this->table_name ), $this->add_id_column_conditions() ]; return self::execute( \implode( ' ', $query ), \is_array( $this->id( true ) ) ? \array_values( $this->id( true ) ) : [ $this->id( true ) ] ); } /** * Deletes many records from the database. * * @return bool|int Response of wpdb::query. */ public function delete_many() { // Build and return the full DELETE statement by concatenating // the results of calling each separate builder method. $query = $this->join_if_not_empty( ' ', [ 'DELETE FROM', $this->quote_identifier( $this->table_name ), $this->build_where(), ] ); return self::execute( $query, $this->values ); } /* * --- ArrayAccess --- */ /** * Checks whether the data has the key. * * @param mixed $offset Key. * * @return bool Whether the data has the key. */ #[ReturnTypeWillChange] public function offsetExists( $offset ) { return \array_key_exists( $offset, $this->data ); } /** * Retrieves the value of the key. * * @param mixed $offset Key. * * @return array|mixed|null The value. */ #[ReturnTypeWillChange] public function offsetGet( $offset ) { return $this->get( $offset ); } /** * Sets the value of the key. * * @param string|int $offset Key. * @param mixed $value Value. */ #[ReturnTypeWillChange] public function offsetSet( $offset, $value ) { if ( \is_null( $offset ) ) { return; } $this->set( $offset, $value ); } /** * Removes the given key from the data. * * @param mixed $offset Key. */ #[ReturnTypeWillChange] public function offsetUnset( $offset ) { unset( $this->data[ $offset ] ); unset( $this->dirty_fields[ $offset ] ); } /* * --- MAGIC METHODS --- */ /** * Handles magic get via offset. * * @param mixed $key Key. * * @return array|mixed|null The value in the offset. */ public function __get( $key ) { return $this->offsetGet( $key ); } /** * Handles magic set via offset. * * @param string|int $key Key. * @param mixed $value Value. */ public function __set( $key, $value ) { $this->offsetSet( $key, $value ); } /** * Handles magic unset via offset. * * @param mixed $key Key. */ public function __unset( $key ) { $this->offsetUnset( $key ); } /** * Handles magic isset via offset. * * @param mixed $key Key. * * @return bool Whether the offset has the key. */ public function __isset( $key ) { return $this->offsetExists( $key ); } }