table = $db->prefix . 'us_questions'; } public function insert( Question $question ): int { $this->db->insert( $this->table, $this->columns( $question ) + [ 'created_at' => current_time( 'mysql' ) ], [ '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s' ] ); return $this->db->insert_id; } public function update( int $id, Question $question ): bool { return false !== $this->db->update( $this->table, $this->columns( $question ), [ 'id' => $id ], [ '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%d' ], [ '%d' ] ); } /** * Column values shared by insert and update (excludes created_at). * * @return array */ private function columns( Question $question ): array { return [ 'offering_id' => $question->offeringId, 'scope' => $question->scope, 'label' => $question->label, 'field_type' => $question->fieldType, 'options' => null === $question->options ? null : (string) wp_json_encode( $question->options ), 'is_required' => $question->isRequired ? 1 : 0, 'sort_order' => $question->sortOrder, 'is_active' => $question->isActive ? 1 : 0, ]; } /** * Find questions for an offering, ordered for display. * * @return list */ public function findByOffering( int $offeringId, bool $activeOnly = false ): array { $sql = 'SELECT * FROM %i WHERE offering_id = %d'; $params = [ $this->table, $offeringId ]; if ( $activeOnly ) { $sql .= ' AND is_active = %d'; $params[] = 1; } $sql .= ' ORDER BY sort_order ASC, id ASC'; $rows = $this->db->get_results( $this->db->prepare( $sql, $params ) ); return array_map( Question::fromRow( ... ), $rows ?? [] ); } /** * Find questions for a scope (e.g. account-signup), ordered for display. * * @return list */ public function findByScope( string $scope, bool $activeOnly = false ): array { $sql = 'SELECT * FROM %i WHERE scope = %s'; $params = [ $this->table, $scope ]; if ( $activeOnly ) { $sql .= ' AND is_active = %d'; $params[] = 1; } $sql .= ' ORDER BY sort_order ASC, id ASC'; $rows = $this->db->get_results( $this->db->prepare( $sql, $params ) ); return array_map( Question::fromRow( ... ), $rows ?? [] ); } public function findById( int $id ): ?Question { $row = $this->db->get_row( $this->db->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table, $id ) ); return $row ? Question::fromRow( $row ) : null; } public function delete( int $id ): bool { return (bool) $this->db->delete( $this->table, [ 'id' => $id ], [ '%d' ] ); } /** * Relax `offering_id` to allow NULL for account-scope questions (which are * not tied to an offering). * * The account-questions feature (v1.1.0) made the column nullable in the * schema, but dbDelta does not reliably change a column from NOT NULL to * NULL, so sites created before then keep the old NOT NULL column and reject * account-scope inserts with "Column 'offering_id' cannot be null". This * MODIFY is idempotent — re-applying the nullable definition is a no-op. * * @return bool True when the statement ran (or was already applied), false * if it could not be prepared or the query failed. */ public function ensureOfferingNullable(): bool { $sql = $this->db->prepare( 'ALTER TABLE %i MODIFY offering_id BIGINT UNSIGNED NULL DEFAULT NULL', $this->table ); return null !== $sql && false !== $this->db->query( $sql ); } }