diff --git a/code/prototype.php b/code/prototype.php new file mode 100644 index 0000000..f80082f --- /dev/null +++ b/code/prototype.php @@ -0,0 +1,40 @@ +addComment("Nice book!"); + + // ... + + $draft = clone $page; + + echo "Dump of the clone. Note that the author is now referencing two objects.\n\n"; + + /** @noinspection ForgottenDebugOutputInspection */ + dump($draft); +} + +client(); diff --git a/public/index.php b/public/index.php index df1f2e9..028f73b 100644 --- a/public/index.php +++ b/public/index.php @@ -1,4 +1,5 @@ @@ -19,7 +20,7 @@ require '../src/helpers.php'; height: 100dvh; margin: 0; padding: 0; - background: #ddd; + background: #e0e5e5; color: #1c1b19; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; } @@ -40,22 +41,36 @@ require '../src/helpers.php'; transform: translate(-50%, -50%); max-height: 92dvh; overflow-y: auto; + background-color: attr(data-color type(), #eef0f0); } - pre { + pre, code { font-family: Consolas, monospace; + font-size: 1.125rem; + } + pre { align-self: center; } img.diagram { max-width: 100%; } + + .et { + position: absolute; + top: .5rem; + left: .5rem; + font-size: small; + font-weight: 500; + color: #333; + } -
+
+
ET: secs
diff --git a/src/Pattern/Creational/Prototype/Author.php b/src/Pattern/Creational/Prototype/Author.php new file mode 100644 index 0000000..b7ca8bb --- /dev/null +++ b/src/Pattern/Creational/Prototype/Author.php @@ -0,0 +1,27 @@ +pages[] = $page; + } +} diff --git a/src/Pattern/Creational/Prototype/Page.php b/src/Pattern/Creational/Prototype/Page.php new file mode 100644 index 0000000..c643ece --- /dev/null +++ b/src/Pattern/Creational/Prototype/Page.php @@ -0,0 +1,60 @@ +author->addToPage($this); + $this->date = new DateTime(); + } + + public function addComment(string $comment): void + { + $this->comments[] = $comment; + } + + /** + * You can control what data you want to carry over to the cloned object. + * + * For instance, when a page is cloned: + * - It gets a new "Copy of ..." title. + * - The author of the page remains the same. Therefore we leave the + * reference to the existing object while adding the cloned page to the list + * of the author's pages. + * - We don't carry over the comments from the old page. + * - We also attach a new date object to the page. + */ + public function __clone(): void + { + $this->title = "КОПІЯ: " . $this->title; + $this->body = "КОПІЯ: " . $this->body; + $this->author->addToPage($this); + $this->comments = []; + $this->date = new DateTime(); + } +} diff --git a/storage/patterns_examples/builder.php b/storage/patterns_examples/builder.php new file mode 100644 index 0000000..0c09177 --- /dev/null +++ b/storage/patterns_examples/builder.php @@ -0,0 +1,165 @@ +select(...)->where(...) + */ +interface SQLQueryBuilder +{ + public function select(string $table, array $fields): SQLQueryBuilder; + + public function where(string $field, string $value, string $operator = '='): SQLQueryBuilder; + + public function limit(int $start, int $offset): SQLQueryBuilder; + + // +100 other SQL syntax methods... + + public function getSQL(): string; +} + +/** + * Each Concrete Builder corresponds to a specific SQL dialect and may implement + * the builder steps a little bit differently from the others. + * + * This Concrete Builder can build SQL queries compatible with MySQL. + */ +class MysqlQueryBuilder implements SQLQueryBuilder +{ + protected $query; + + protected function reset(): void + { + $this->query = new \stdClass(); + } + + /** + * Build a base SELECT query. + */ + public function select(string $table, array $fields): SQLQueryBuilder + { + $this->reset(); + $this->query->base = "SELECT " . implode(", ", $fields) . " FROM " . $table; + $this->query->type = 'select'; + + return $this; + } + + /** + * Add a WHERE condition. + */ + public function where(string $field, string $value, string $operator = '='): SQLQueryBuilder + { + if (!in_array($this->query->type, ['select', 'update', 'delete'])) { + throw new \Exception("WHERE can only be added to SELECT, UPDATE OR DELETE"); + } + $this->query->where[] = "$field $operator '$value'"; + + return $this; + } + + /** + * Add a LIMIT constraint. + */ + public function limit(int $start, int $offset): SQLQueryBuilder + { + if (!in_array($this->query->type, ['select'])) { + throw new \Exception("LIMIT can only be added to SELECT"); + } + $this->query->limit = " LIMIT " . $start . ", " . $offset; + + return $this; + } + + /** + * Get the final query string. + */ + public function getSQL(): string + { + $query = $this->query; + $sql = $query->base; + if (!empty($query->where)) { + $sql .= " WHERE " . implode(' AND ', $query->where); + } + if (isset($query->limit)) { + $sql .= $query->limit; + } + $sql .= ";"; + return $sql; + } +} + +/** + * This Concrete Builder is compatible with PostgreSQL. While Postgres is very + * similar to Mysql, it still has several differences. To reuse the common code, + * we extend it from the MySQL builder, while overriding some of the building + * steps. + */ +class PostgresQueryBuilder extends MysqlQueryBuilder +{ + /** + * Among other things, PostgreSQL has slightly different LIMIT syntax. + */ + public function limit(int $start, int $offset): SQLQueryBuilder + { + parent::limit($start, $offset); + + $this->query->limit = " LIMIT " . $start . " OFFSET " . $offset; + + return $this; + } + + // + tons of other overrides... +} + + +/** + * Note that the client code uses the builder object directly. A designated + * Director class is not necessary in this case, because the client code needs + * different queries almost every time, so the sequence of the construction + * steps cannot be easily reused. + * + * Since all our query builders create products of the same type (which is a + * string), we can interact with all builders using their common interface. + * Later, if we implement a new Builder class, we will be able to pass its + * instance to the existing client code without breaking it thanks to the + * SQLQueryBuilder interface. + */ +function clientCode(SQLQueryBuilder $queryBuilder) +{ + // ... + + $query = $queryBuilder + ->select("users", ["name", "email", "password"]) + ->where("age", 18, ">") + ->where("age", 30, "<") + ->limit(10, 20) + ->getSQL(); + + echo $query; + + // ... +} + + +/** + * The application selects the proper query builder type depending on a current + * configuration or the environment settings. + */ +// if ($_ENV['database_type'] == 'postgres') { +// $builder = new PostgresQueryBuilder(); } else { +// $builder = new MysqlQueryBuilder(); } +// +// clientCode($builder); + + +echo "Testing MySQL query builder:\n"; +clientCode(new MysqlQueryBuilder()); + +echo "\n\n"; + +echo "Testing PostgresSQL query builder:\n"; +clientCode(new PostgresQueryBuilder()); \ No newline at end of file