-
Notifications
You must be signed in to change notification settings - Fork 145
Add DatabaseObjectBuilder with TagBuilder implementation
#6689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 6.3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| <?php | ||
|
|
||
| namespace wcf\data; | ||
|
|
||
| use wcf\system\database\exception\DatabaseQueryExecutionException; | ||
| use wcf\system\database\util\PreparedStatementConditionBuilder; | ||
| use wcf\system\exception\ClassNotFoundException; | ||
| use wcf\system\exception\ImplementationException; | ||
| use wcf\system\WCF; | ||
|
|
||
| /** | ||
| * Abstract builder for creating, updating and deleting database objects. | ||
| * | ||
| * @author Marcel Werk | ||
| * @copyright 2001-2026 WoltLab GmbH | ||
| * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | ||
| * @since 6.3 | ||
| * | ||
| * @template TDatabaseObject of DatabaseObject | ||
| */ | ||
| abstract class DatabaseObjectBuilder | ||
| { | ||
| /** | ||
| * @var array<string, string|int|float|null> | ||
| */ | ||
| protected array $properties = []; | ||
|
|
||
| /** | ||
| * @var array<string, string|int|float|null> | ||
| */ | ||
| protected array $customProperties = []; | ||
|
|
||
| /** | ||
| * Use forCreate() or forUpdate() to obtain a builder instance. | ||
| * | ||
| * @param ?TDatabaseObject $object | ||
| */ | ||
| private function __construct(protected readonly ?DatabaseObject $object = null) {} | ||
|
|
||
| /** | ||
| * Persists the pending changes and returns the resulting database object. | ||
| * | ||
| * @return TDatabaseObject | ||
| */ | ||
| public function save(): DatabaseObject | ||
| { | ||
| return new (static::getBaseClass())($this->fastSave()); | ||
| } | ||
|
|
||
| /** | ||
| * Persists the pending changes and returns the object's identifier without | ||
| * instantiating the full database object. | ||
| */ | ||
| public function fastSave(): int|string | ||
| { | ||
| if ($this->object !== null) { | ||
| $this->update(); | ||
|
|
||
| return $this->object->getObjectID(); | ||
| } | ||
|
|
||
| return $this->create(); | ||
| } | ||
|
|
||
| /** | ||
| * Inserts a new row and returns the primary key of the created object. | ||
| */ | ||
| private function create(): int|string | ||
| { | ||
| $keys = $values = ''; | ||
| $statementParameters = []; | ||
| foreach (array_merge($this->properties, $this->customProperties) as $key => $value) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
| if ($keys !== '') { | ||
| $keys .= ','; | ||
| $values .= ','; | ||
| } | ||
|
|
||
| $keys .= $key; | ||
| $values .= '?'; | ||
| $statementParameters[] = $value; | ||
| } | ||
|
|
||
| $sql = "INSERT INTO " . static::getBaseClass()::getDatabaseTableName() . " | ||
| (" . $keys . ") | ||
| VALUES (" . $values . ")"; | ||
| $statement = WCF::getDB()->prepare($sql); | ||
| $statement->execute($statementParameters); | ||
|
|
||
| if (static::getBaseClass()::getDatabaseTableIndexIsIdentity()) { | ||
| $id = WCF::getDB()->getInsertID(static::getBaseClass()::getDatabaseTableName(), static::getBaseClass()::getDatabaseTableIndexName()); | ||
| } elseif (isset($this->properties[static::getBaseClass()::getDatabaseTableIndexName()])) { | ||
| $id = $this->properties[static::getBaseClass()::getDatabaseTableIndexName()]; | ||
| } else { | ||
| throw new \BadMethodCallException("Missing value for '" . static::getBaseClass()::getDatabaseTableIndexName() . "'"); | ||
| } | ||
|
|
||
| return $id; | ||
|
Comment on lines
+89
to
+97
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be untangled into two conditional returns and an unconditional exception. The |
||
| } | ||
|
|
||
| /** | ||
| * Writes the pending property changes to the existing row. | ||
| */ | ||
| private function update(): void | ||
| { | ||
| if ($this->properties === [] && $this->customProperties === []) { | ||
| return; | ||
| } | ||
|
|
||
| $updateSQL = ''; | ||
| $statementParameters = []; | ||
| foreach (array_merge($this->properties, $this->customProperties) as $key => $value) { | ||
| if ($updateSQL !== '') { | ||
| $updateSQL .= ', '; | ||
| } | ||
| $updateSQL .= $key . ' = ?'; | ||
| $statementParameters[] = $value; | ||
| } | ||
| $statementParameters[] = $this->object->getObjectID(); | ||
|
|
||
| $sql = "UPDATE " . static::getBaseClass()::getDatabaseTableName() . " | ||
| SET " . $updateSQL . " | ||
| WHERE " . static::getBaseClass()::getDatabaseTableIndexName() . " = ?"; | ||
| $statement = WCF::getDB()->prepare($sql); | ||
| $statement->execute($statementParameters); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new object, returns null if the row already exists. | ||
| * | ||
| * @return ?TDatabaseObject | ||
| */ | ||
| public function createOrIgnore(): ?DatabaseObject | ||
| { | ||
| if ($this->object !== null) { | ||
| throw new \BadMethodCallException("createOrIgnore() can only be used with forCreate()."); | ||
| } | ||
|
|
||
| try { | ||
| return $this->save(); | ||
| } catch (DatabaseQueryExecutionException $e) { | ||
| // Error code 23000 = duplicate key | ||
| if (\intval($e->getCode()) === 23000 && $e->getDriverCode() === '1062') { | ||
| return null; | ||
| } | ||
|
|
||
| throw $e; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Deletes the given database object. | ||
| * | ||
| * @param TDatabaseObject $object | ||
| */ | ||
| public static function delete(DatabaseObject $object): void | ||
| { | ||
| static::deleteAll([$object->getObjectID()]); | ||
| } | ||
|
|
||
| /** | ||
| * Deletes the rows identified by the given primary keys in batches inside | ||
| * a single transaction. | ||
| * | ||
| * @param (string|int)[] $objectIDs | ||
| */ | ||
| public static function deleteAll(array $objectIDs = []): void | ||
| { | ||
| if ($objectIDs === []) { | ||
| return; | ||
| } | ||
|
|
||
| $itemsPerLoop = 1000; | ||
| $loopCount = \ceil(\count($objectIDs) / $itemsPerLoop); | ||
|
|
||
| WCF::getDB()->beginTransaction(); | ||
| $committed = false; | ||
| try { | ||
| for ($i = 0; $i < $loopCount; $i++) { | ||
| $batchObjectIDs = \array_slice($objectIDs, $i * $itemsPerLoop, $itemsPerLoop); | ||
|
|
||
| $conditionBuilder = new PreparedStatementConditionBuilder(); | ||
| $conditionBuilder->add(static::getBaseClass()::getDatabaseTableIndexName() . ' IN (?)', [$batchObjectIDs]); | ||
|
|
||
| $sql = "DELETE FROM " . static::getBaseClass()::getDatabaseTableName() . " | ||
| " . $conditionBuilder; | ||
| $statement = WCF::getDB()->prepare($sql); | ||
| $statement->execute($conditionBuilder->getParameters()); | ||
| } | ||
| WCF::getDB()->commitTransaction(); | ||
| $committed = true; | ||
| } finally { | ||
| if (!$committed) { | ||
| WCF::getDB()->rollBackTransaction(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns a builder instance for inserting a new row. | ||
| */ | ||
| public static function forCreate(): static | ||
| { | ||
| return new (static::class)(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a builder instance for updating an existing database object. | ||
| * | ||
| * @param TDatabaseObject $object | ||
| */ | ||
| public static function forUpdate(DatabaseObject $object): static | ||
| { | ||
| return new static($object); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the database object class associated with this builder by | ||
| * stripping the `Builder` suffix from the current class name. | ||
| * | ||
| * @return class-string<DatabaseObject> | ||
| */ | ||
| public static function getBaseClass(): string | ||
| { | ||
| if (!\str_ends_with(static::class, 'Builder')) { | ||
| throw new \LogicException("Builder class '" . static::class . "' must end with the 'Builder' suffix."); | ||
| } | ||
|
|
||
| $className = \mb_substr(static::class, 0, -7); | ||
| if (!\class_exists($className)) { | ||
| throw new ClassNotFoundException($className); | ||
| } | ||
|
|
||
| if (!\is_subclass_of($className, DatabaseObject::class)) { | ||
| throw new ImplementationException($className, DatabaseObject::class); | ||
| } | ||
|
|
||
| return $className; | ||
| } | ||
|
|
||
| /** | ||
| * Sets a custom property value that is written alongside the regular | ||
| * properties when the object is persisted. | ||
| */ | ||
| public function setCustomProperty(string $name, string|int|float|null $value): static | ||
| { | ||
| $this->customProperties[$name] = $value; | ||
|
|
||
| return $this; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <?php | ||
|
|
||
| namespace wcf\data\tag; | ||
|
|
||
| use wcf\data\DatabaseObjectBuilder; | ||
|
|
||
| /** | ||
| * Builder for creating, updating and deleting tags. | ||
| * | ||
| * @author Marcel Werk | ||
| * @copyright 2001-2026 WoltLab GmbH | ||
| * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | ||
| * @since 6.3 | ||
| * | ||
| * @extends DatabaseObjectBuilder<Tag> | ||
| */ | ||
| final class TagBuilder extends DatabaseObjectBuilder | ||
| { | ||
| public function setTagID(int $tagID): static | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This must not be available inside the |
||
| { | ||
| $this->properties['tagID'] = $tagID; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| public function setLanguageID(int $languageID): static | ||
| { | ||
| $this->properties['languageID'] = $languageID; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| public function setName(string $name): static | ||
| { | ||
| $this->properties['name'] = $name; | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| public function setSynonymFor(Tag $tag): static | ||
| { | ||
| $this->properties['synonymFor'] = $tag->tagID; | ||
|
|
||
| return $this; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and all other methods declared by
DatabaseObjectBuildershould be declared asfinalto allow us to make adjustments to this crucial component.