diff --git a/src/CacheKey.php b/src/CacheKey.php index c4b985f..441d002 100644 --- a/src/CacheKey.php +++ b/src/CacheKey.php @@ -51,11 +51,66 @@ public function make( return $key; } + protected function getBindingsSlug() : string + { + if (! method_exists($this->model, 'query')) { + return ''; + } + + return Arr::query($this->model->query()->getBindings()); + } + + protected function getColumnClauses(array $where) : string + { + if ($where["type"] !== "Column") { + return ""; + } + + return "-{$where["boolean"]}_{$where["first"]}_{$where["operator"]}_{$where["second"]}"; + } + + protected function getCurrentBinding(string $type, $bindingFallback = null) + { + return data_get($this->query->bindings, "{$type}.{$this->currentBinding}", $bindingFallback); + } + protected function getIdColumn(string $idColumn) : string { return $idColumn ? "_{$idColumn}" : ""; } + protected function getInAndNotInClauses(array $where) : string + { + if (! in_array($where["type"], ["In", "NotIn", "InRaw"])) { + return ""; + } + + $type = strtolower($where["type"]); + $subquery = $this->getValuesFromWhere($where); + $values = collect($this->getCurrentBinding('where', [])); + + if (Str::startsWith($subquery, $values->first())) { + $this->currentBinding += count($where["values"]); + } + + if (! is_numeric($subquery) && ! is_numeric(str_replace("_", "", $subquery))) { + try { + $subquery = Uuid::fromBytes($subquery); + $values = $this->recursiveImplode([$subquery], "_"); + + return "-{$where["column"]}_{$type}{$values}"; + } catch (Exception $exception) { + // do nothing + } + } + + $subquery = preg_replace('/\?(?=(?:[^"]*"[^"]*")*[^"]*\Z)/m', "_??_", $subquery); + $subquery = collect(vsprintf(str_replace("_??_", "%s", $subquery), $values->toArray())); + $values = $this->recursiveImplode($subquery->toArray(), "_"); + + return "-{$where["column"]}_{$type}{$values}"; + } + protected function getLimitClause() : string { if (! property_exists($this->query, "limit") @@ -67,15 +122,18 @@ protected function getLimitClause() : string return "-limit_{$this->query->limit}"; } - protected function getTableSlug() : string + protected function getModelSlug() : string { - return (new Str)->slug($this->query->from) - . ":"; + return (new Str)->slug(get_class($this->model)); } - protected function getModelSlug() : string + protected function getNestedClauses(array $where) : string { - return (new Str)->slug(get_class($this->model)); + if (! in_array($where["type"], ["Exists", "Nested", "NotExists"])) { + return ""; + } + + return "-" . strtolower($where["type"]) . $this->getWhereClauses($where["query"]->wheres); } protected function getOffsetClause() : string @@ -110,6 +168,18 @@ protected function getOrderByClauses() : string ?: ""; } + protected function getOtherClauses(array $where) : string + { + if (in_array($where["type"], ["Exists", "Nested", "NotExists", "Column", "raw", "In", "NotIn", "InRaw"])) { + return ""; + } + + $value = $this->getTypeClause($where); + $value .= $this->getValuesClause($where); + + return "-{$where["column"]}_{$value}"; + } + protected function getQueryColumns(array $columns) : string { if (($columns === ["*"] @@ -129,6 +199,36 @@ protected function getQueryColumns(array $columns) : string return "_" . implode("_", $columns); } + protected function getRawClauses(array $where) : string + { + if (! in_array($where["type"], ["raw"])) { + return ""; + } + + $queryParts = explode("?", $where["sql"]); + $clause = "_{$where["boolean"]}"; + + while (count($queryParts) > 1) { + $clause .= "_" . array_shift($queryParts); + $clause .= $this->getCurrentBinding("where"); + $this->currentBinding++; + } + + $lastPart = array_shift($queryParts); + + if ($lastPart) { + $clause .= "_" . $lastPart; + } + + return "-" . str_replace(" ", "_", $clause); + } + + protected function getTableSlug() : string + { + return (new Str)->slug($this->query->from) + . ":"; + } + protected function getTypeClause($where) : string { $type = in_array($where["type"], ["InRaw", "In", "NotIn", "Null", "NotNull", "between", "NotInSub", "InSub", "JsonContains"]) @@ -174,12 +274,15 @@ protected function getValuesFromWhere(array $where) : string protected function getValuesFromBindings(array $where, string $values) : string { - if (($this->query->bindings["where"][$this->currentBinding] ?? false) !== false) { - $values = $this->query->bindings["where"][$this->currentBinding]; + $bindingFallback = __CLASS__ . ':UNKNOWN_BINDING'; + $currentBinding = $this->getCurrentBinding("where", $bindingFallback); + + if ($currentBinding !== $bindingFallback) { + $values = $currentBinding; $this->currentBinding++; if ($where["type"] === "between") { - $values .= "_" . $this->query->bindings["where"][$this->currentBinding]; + $values .= "_" . $this->getCurrentBinding("where"); $this->currentBinding++; } } @@ -207,57 +310,41 @@ protected function getWhereClauses(array $wheres = []) : string return $value; }); } - - protected function getNestedClauses(array $where) : string + + protected function getWheres(array $wheres) : Collection { - if (! in_array($where["type"], ["Exists", "Nested", "NotExists"])) { - return ""; - } - - return "-" . strtolower($where["type"]) . $this->getWhereClauses($where["query"]->wheres); - } + $wheres = collect($wheres); - protected function getColumnClauses(array $where) : string - { - if ($where["type"] !== "Column") { - return ""; + if ($wheres->isEmpty() + && property_exists($this->query, "wheres") + ) { + $wheres = collect($this->query->wheres); } - return "-{$where["boolean"]}_{$where["first"]}_{$where["operator"]}_{$where["second"]}"; + return $wheres; } - protected function getInAndNotInClauses(array $where) : string + protected function getWithModels() : string { - if (! in_array($where["type"], ["In", "NotIn", "InRaw"])) { - return ""; - } - - $type = strtolower($where["type"]); - $subquery = $this->getValuesFromWhere($where); - $values = collect($this->query->bindings["where"][$this->currentBinding] ?? []); + $eagerLoads = collect($this->eagerLoad); - if (Str::startsWith($subquery, $values->first())) { - $this->currentBinding += count($where["values"]); + if ($eagerLoads->isEmpty()) { + return ""; } - if (! is_numeric($subquery) && ! is_numeric(str_replace("_", "", $subquery))) { - try { - $subquery = Uuid::fromBytes($subquery); - $values = $this->recursiveImplode([$subquery], "_"); - - return "-{$where["column"]}_{$type}{$values}"; - } catch (Exception $exception) { - // do nothing + return $eagerLoads->keys()->reduce(function ($carry, $related) { + if (! method_exists($this->model, $related)) { + return "{$carry}-{$related}"; } - } - $subquery = preg_replace('/\?(?=(?:[^"]*"[^"]*")*[^"]*\Z)/m', "_??_", $subquery); - $subquery = collect(vsprintf(str_replace("_??_", "%s", $subquery), $values->toArray())); - $values = $this->recursiveImplode($subquery->toArray(), "_"); + $relatedModel = $this->model->$related()->getRelated(); + $relatedConnection = $relatedModel->getConnection()->getName(); + $relatedDatabase = $relatedModel->getConnection()->getDatabaseName(); - return "-{$where["column"]}_{$type}{$values}"; + return "{$carry}-{$relatedConnection}:{$relatedDatabase}:{$related}"; + }); } - + protected function recursiveImplode(array $items, string $glue = ",") : string { $result = ""; @@ -283,83 +370,4 @@ protected function recursiveImplode(array $items, string $glue = ",") : string return $result; } - - protected function getRawClauses(array $where) : string - { - if (! in_array($where["type"], ["raw"])) { - return ""; - } - - $queryParts = explode("?", $where["sql"]); - $clause = "_{$where["boolean"]}"; - - while (count($queryParts) > 1) { - $clause .= "_" . array_shift($queryParts); - $clause .= $this->query->bindings["where"][$this->currentBinding]; - $this->currentBinding++; - } - - $lastPart = array_shift($queryParts); - - if ($lastPart) { - $clause .= "_" . $lastPart; - } - - return "-" . str_replace(" ", "_", $clause); - } - - protected function getOtherClauses(array $where) : string - { - if (in_array($where["type"], ["Exists", "Nested", "NotExists", "Column", "raw", "In", "NotIn", "InRaw"])) { - return ""; - } - - $value = $this->getTypeClause($where); - $value .= $this->getValuesClause($where); - - return "-{$where["column"]}_{$value}"; - } - - protected function getWheres(array $wheres) : Collection - { - $wheres = collect($wheres); - - if ($wheres->isEmpty() - && property_exists($this->query, "wheres") - ) { - $wheres = collect($this->query->wheres); - } - - return $wheres; - } - - protected function getWithModels() : string - { - $eagerLoads = collect($this->eagerLoad); - - if ($eagerLoads->isEmpty()) { - return ""; - } - - return $eagerLoads->keys()->reduce(function ($carry, $related) { - if (! method_exists($this->model, $related)) { - return "{$carry}-{$related}"; - } - - $relatedModel = $this->model->$related()->getRelated(); - $relatedConnection = $relatedModel->getConnection()->getName(); - $relatedDatabase = $relatedModel->getConnection()->getDatabaseName(); - - return "{$carry}-{$relatedConnection}:{$relatedDatabase}:{$related}"; - }); - } - - protected function getBindingsSlug() : string - { - if (! method_exists($this->model, 'query')) { - return ''; - } - - return Arr::query($this->model->query()->getBindings()); - } } diff --git a/tests/Integration/CachedBuilder/BooleanTest.php b/tests/Integration/CachedBuilder/BooleanTest.php index f03188e..93079fd 100644 --- a/tests/Integration/CachedBuilder/BooleanTest.php +++ b/tests/Integration/CachedBuilder/BooleanTest.php @@ -1,12 +1,14 @@ testingSqlitePath}testing.sqlite:authors:genealabslaravelmodelcachingtestsfixturesauthor-is_famous_=_1-authors.deleted_at_null"); $tags = [ @@ -29,4 +31,61 @@ public function testBooleanWhereCreatesCorrectCacheKey() $this->assertNotEmpty($cachedResults); $this->assertNotEmpty($liveResults); } + + public function testBooleanWhereFalseCreatesCorrectCacheKey() + { + $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:authors:genealabslaravelmodelcachingtestsfixturesauthor-is_famous_=_-authors.deleted_at_null"); + $tags = [ + "genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:genealabslaravelmodelcachingtestsfixturesauthor", + ]; + + $authors = (new Author) + ->where("is_famous", false) + ->get(); + $cachedResults = $this->cache() + ->tags($tags) + ->get($key)['value']; + $liveResults = (new UncachedAuthor) + ->where("is_famous", false) + ->get(); + + $this->assertEquals($liveResults->pluck("id"), $authors->pluck("id")); + $this->assertEquals($liveResults->pluck("id"), $cachedResults->pluck("id")); + $this->assertNotEmpty($authors); + $this->assertNotEmpty($cachedResults); + $this->assertNotEmpty($liveResults); + } + + public function testBooleanWhereHasRelationWithFalseConditionAndAdditionalParentRawCondition() + { + $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:books:genealabslaravelmodelcachingtestsfixturesbook-exists-and_books.author_id_=_authors.id-is_famous_=_-authors.deleted_at_null-_and_title_=_Mixed_Clause"); + $tags = [ + "genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:genealabslaravelmodelcachingtestsfixturesbook", + ]; + + $expectedAuthor = factory(Author::class)->create(['is_famous' => false]); + factory(Book::class)->create(['author_id' => $expectedAuthor->getKey(), 'title' => 'Mixed Clause']); + + $books = (new Book) + ->whereHas('author', function ($query) { + return $query->where('is_famous', false); + }) + ->whereRaw("title = ?", ['Mixed Clause']) // Test ensures this binding is included in the key + ->get(); + $cachedResults = $this->cache() + ->tags($tags) + ->get($key)['value']; + $liveResults = (new UncachedBook) + ->whereHas('author', function ($query) { + return $query->where('is_famous', false); + }) + ->whereRaw("title = ?", ['Mixed Clause']) + ->get(); + + $this->assertEquals($liveResults->pluck("id"), $books->pluck("id")); + $this->assertEquals($liveResults->pluck("id"), $cachedResults->pluck("id")); + $this->assertNotEmpty($books); + $this->assertNotEmpty($cachedResults); + $this->assertNotEmpty($liveResults); + } } diff --git a/tests/Integration/CachedBuilder/PolymorphicOneToManyTest.php b/tests/Integration/CachedBuilder/PolymorphicOneToManyTest.php index 0a28cdb..93725bd 100644 --- a/tests/Integration/CachedBuilder/PolymorphicOneToManyTest.php +++ b/tests/Integration/CachedBuilder/PolymorphicOneToManyTest.php @@ -37,7 +37,7 @@ public function testEagerloadedRelationship() public function testLazyloadedRelationship() { - $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:comments:genealabslaravelmodelcachingtestsfixturescomment-comments.commentable_id_=_1-comments.commentable_id_notnull-comments.commentable_type_=_GeneaLabs\LaravelModelCaching\Tests\Fixtures\Post"); + $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:comments:genealabslaravelmodelcachingtestsfixturescomment-comments.commentable_type_=_GeneaLabs\LaravelModelCaching\Tests\Fixtures\Post-comments.commentable_id_=_1-comments.commentable_id_notnull"); $tags = [ "genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:genealabslaravelmodelcachingtestsfixturescomment", ]; diff --git a/tests/Integration/CachedBuilder/PolymorphicOneToOneTest.php b/tests/Integration/CachedBuilder/PolymorphicOneToOneTest.php index 5c9f25f..9272058 100644 --- a/tests/Integration/CachedBuilder/PolymorphicOneToOneTest.php +++ b/tests/Integration/CachedBuilder/PolymorphicOneToOneTest.php @@ -37,7 +37,7 @@ public function testEagerloadedRelationship() public function testLazyloadedHasOneThrough() { - $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:images:genealabslaravelmodelcachingtestsfixturesimage-images.imagable_id_=_2-images.imagable_id_notnull-images.imagable_type_=_GeneaLabs\LaravelModelCaching\Tests\Fixtures\User-limit_1"); + $key = sha1("genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:images:genealabslaravelmodelcachingtestsfixturesimage-images.imagable_type_=_GeneaLabs\LaravelModelCaching\Tests\Fixtures\User-images.imagable_id_=_2-images.imagable_id_notnull-limit_1"); $tags = [ "genealabs:laravel-model-caching:testing:{$this->testingSqlitePath}testing.sqlite:genealabslaravelmodelcachingtestsfixturesimage", ];