Skip to content

Commit 02e68ec

Browse files
committed
Add public prop to bars
1 parent d12132e commit 02e68ec

File tree

10 files changed

+344
-12
lines changed

10 files changed

+344
-12
lines changed

app/Http/Controllers/Public/CocktailController.php

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,62 @@
44

55
namespace Kami\Cocktail\Http\Controllers\Public;
66

7+
use Illuminate\Http\Request;
8+
use Kami\Cocktail\Models\Bar;
79
use Kami\Cocktail\Models\Cocktail;
10+
use Illuminate\Support\Facades\Cache;
811
use Kami\Cocktail\Http\Controllers\Controller;
12+
use Illuminate\Http\Resources\Json\JsonResource;
13+
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
14+
use Kami\Cocktail\Http\Filters\PublicCocktailQueryFilter;
915
use Kami\Cocktail\Http\Resources\ExploreCocktailResource;
16+
use Kami\Cocktail\Http\Resources\Public\CocktailResource;
1017

1118
class CocktailController extends Controller
1219
{
13-
/**
14-
* @return array<string>
15-
*/
16-
public function index(string $barSlug): array
20+
public function index(Request $request, int $barId): JsonResource
1721
{
18-
return [$barSlug];
22+
$bar = Bar::findOrFail($barId);
23+
24+
if (!$bar->isPublic()) {
25+
abort(404);
26+
}
27+
28+
try {
29+
$cocktailsQuery = new PublicCocktailQueryFilter($bar);
30+
} catch (InvalidFilterQuery $e) {
31+
abort(400, $e->getMessage());
32+
}
33+
34+
$queryParams = $request->only([
35+
'id',
36+
'name',
37+
'ingredient_name',
38+
'ingredient_substitute_id',
39+
'ingredient_id',
40+
'tag_id',
41+
'created_user_id',
42+
'glass_id',
43+
'cocktail_method_id',
44+
'bar_shelf',
45+
'abv_min',
46+
'abv_max',
47+
'main_ingredient_id',
48+
'total_ingredients',
49+
'parent_cocktail_id',
50+
'per_page',
51+
'sort',
52+
'page',
53+
]);
54+
ksort($queryParams);
55+
$queryString = http_build_query($queryParams);
56+
$cacheKey = 'public_cocktails_' . $barId . '_' . sha1($queryString);
57+
58+
$cocktails = Cache::remember($cacheKey, 3600, function () use ($cocktailsQuery, $request) {
59+
return $cocktailsQuery->paginate($request->get('per_page', 25));
60+
});
61+
62+
return CocktailResource::collection($cocktails->withQueryString());
1963
}
2064

2165
public function show(string $barSlug, string $id): ExploreCocktailResource

app/Http/Controllers/Public/MenuController.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@
55
namespace Kami\Cocktail\Http\Controllers\Public;
66

77
use Kami\Cocktail\Models\Menu;
8+
use OpenApi\Attributes as OAT;
9+
use Kami\Cocktail\OpenAPI as BAO;
810
use Kami\Cocktail\Http\Controllers\Controller;
911
use Kami\Cocktail\Http\Resources\MenuPublicResource;
1012

1113
class MenuController extends Controller
1214
{
13-
public function show(string $barSlug): MenuPublicResource
15+
#[OAT\Get(path: '/public/{barId}/menu', tags: ['Public'], operationId: 'publicBarMenu', description: 'Show a public bar menu details', summary: 'Show public menu', parameters: [
16+
new OAT\Parameter(name: 'barId', in: 'path', required: true, description: 'Bar database id', schema: new OAT\Schema(type: 'number')),
17+
], security: [])]
18+
#[BAO\SuccessfulResponse(content: [
19+
new BAO\WrapObjectWithData(MenuPublicResource::class),
20+
])]
21+
#[BAO\NotFoundResponse]
22+
public function show(string $barId): MenuPublicResource
1423
{
1524
$menu = Menu::select('menus.*')
16-
->where(['slug' => $barSlug])
25+
->where('bars.id', $barId)
1726
->where('menus.is_enabled', true)
1827
->join('bars', 'bars.id', '=', 'menus.bar_id')
1928
->join('menu_cocktails', 'menu_cocktails.menu_id', '=', 'menus.id')
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kami\Cocktail\Http\Filters;
6+
7+
use Kami\Cocktail\Models\Bar;
8+
use Kami\Cocktail\Models\Cocktail;
9+
use Spatie\QueryBuilder\AllowedSort;
10+
use Spatie\QueryBuilder\QueryBuilder;
11+
use Spatie\QueryBuilder\AllowedFilter;
12+
use Spatie\QueryBuilder\AllowedInclude;
13+
14+
/**
15+
* @extends \Spatie\QueryBuilder\QueryBuilder<Cocktail>
16+
*/
17+
final class PublicCocktailQueryFilter extends QueryBuilder
18+
{
19+
public function __construct(Bar $bar)
20+
{
21+
parent::__construct(Cocktail::query());
22+
23+
$this
24+
->allowedFilters([
25+
AllowedFilter::exact('id'),
26+
AllowedFilter::custom('name', new FilterNameSearch()),
27+
AllowedFilter::partial('ingredient_name', 'ingredients.ingredient.name'),
28+
AllowedFilter::exact('ingredient_substitute_id', 'ingredients.substitutes.ingredient.id'),
29+
AllowedFilter::callback('ingredient_id', function ($query, $value) {
30+
if (!is_array($value)) {
31+
$value = [$value];
32+
}
33+
34+
$query->where(function ($q) use ($value) {
35+
$q->whereIn('ci.ingredient_id', $value)->orWhereIn('cis.ingredient_id', $value);
36+
});
37+
}),
38+
AllowedFilter::exact('tag_id', 'tags.id'),
39+
AllowedFilter::exact('created_user_id'),
40+
AllowedFilter::exact('glass_id'),
41+
AllowedFilter::exact('cocktail_method_id'),
42+
AllowedFilter::callback('bar_shelf', function ($query, $value) use ($bar) {
43+
if ($value === true) {
44+
$query->whereIn('cocktails.id', $bar->getShelfCocktailsOnce());
45+
}
46+
}),
47+
AllowedFilter::callback('abv_min', function ($query, $value) {
48+
$query->where('abv', '>=', $value);
49+
}),
50+
AllowedFilter::callback('abv_max', function ($query, $value) {
51+
$query->where('abv', '<=', $value);
52+
}),
53+
AllowedFilter::callback('main_ingredient_id', function ($query, $value) {
54+
if (!is_array($value)) {
55+
$value = [$value];
56+
}
57+
58+
$query->whereIn('ci.ingredient_id', $value)->where('sort', '=', 1);
59+
}),
60+
AllowedFilter::callback('total_ingredients', function ($query, $value) {
61+
$query->having('total_ingredients', '>=', (int) $value);
62+
}),
63+
AllowedFilter::exact('parent_cocktail_id'),
64+
])
65+
->defaultSort('name')
66+
->allowedSorts([
67+
'name',
68+
'created_at',
69+
'abv',
70+
'total_ingredients',
71+
AllowedSort::callback('random', function ($query) {
72+
$query->inRandomOrder();
73+
}),
74+
])
75+
->allowedIncludes([
76+
'glass',
77+
'method',
78+
'user',
79+
'utensils',
80+
'images',
81+
'tags',
82+
'ingredients.ingredient',
83+
])
84+
->select('cocktails.*')
85+
->leftJoin('cocktail_ingredients AS ci', 'ci.cocktail_id', '=', 'cocktails.id')
86+
->leftJoin('cocktail_ingredient_substitutes AS cis', 'cis.cocktail_ingredient_id', '=', 'ci.id')
87+
->leftJoin('bar_ingredients AS bi', function ($query) {
88+
$query->on('bi.ingredient_id', '=', 'ci.ingredient_id');
89+
})
90+
->where('cocktails.bar_id', $bar->id)
91+
->groupBy('cocktails.id')
92+
->with(
93+
'bar.shelfIngredients',
94+
'ingredients.ingredient.bar',
95+
'tags',
96+
'ingredients.substitutes.ingredient',
97+
'glass',
98+
'method',
99+
'utensils',
100+
'images',
101+
);
102+
}
103+
}

app/Http/Resources/BarResource.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
'can_deactivate',
4545
]),
4646
new OAT\Property(property: 'images', type: 'array', items: new OAT\Items(type: ImageResource::class), description: 'Images associated with the bar'),
47+
new OAT\Property(property: 'is_public', type: 'boolean', default: false, example: true),
4748
],
4849
required: [
4950
'id',
@@ -59,6 +60,7 @@
5960
'created_at',
6061
'updated_at',
6162
'access',
63+
'is_public',
6264
]
6365
)]
6466
class BarResource extends JsonResource
@@ -97,6 +99,7 @@ public function toArray($request)
9799
$this->relationLoaded('images'),
98100
fn () => ImageResource::collection($this->images)
99101
),
102+
'is_public' => $this->isPublic(),
100103
];
101104
}
102105
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kami\Cocktail\Http\Resources\Public;
6+
7+
use Kami\Cocktail\Models\Image;
8+
use Kami\Cocktail\Models\CocktailIngredient;
9+
use Kami\Cocktail\Http\Resources\AmountFormats;
10+
use Illuminate\Http\Resources\Json\JsonResource;
11+
use Kami\Cocktail\Models\CocktailIngredientSubstitute;
12+
13+
/**
14+
* @mixin \Kami\Cocktail\Models\Cocktail
15+
*/
16+
class CocktailResource extends JsonResource
17+
{
18+
/**
19+
* Transform the resource into an array.
20+
*
21+
* @param \Illuminate\Http\Request $request
22+
* @return array<string, mixed>
23+
*/
24+
public function toArray($request)
25+
{
26+
return [
27+
'name' => $this->name,
28+
'slug' => $this->slug,
29+
'instructions' => e($this->instructions),
30+
'garnish' => e($this->garnish),
31+
'description' => e($this->description),
32+
'source' => $this->source,
33+
'public_id' => $this->public_id,
34+
'public_at' => $this->public_at?->toAtomString() ?? null,
35+
'images' => $this->images->map(function (Image $image) {
36+
return [
37+
'sort' => $image->sort,
38+
'placeholder_hash' => $image->placeholder_hash,
39+
'url' => $image->getImageUrl(),
40+
'copyright' => $image->copyright,
41+
];
42+
}),
43+
'tags' => $this->when(
44+
$this->relationLoaded('tags'),
45+
fn () => $this->tags->map(function ($tag) {
46+
return $tag->name;
47+
})
48+
),
49+
'glass' => $this->glass->name ?? null,
50+
'utensils' => $this->utensils->pluck('name'),
51+
'method' => $this->method->name ?? null,
52+
'ingredients' => $this->ingredients->map(function (CocktailIngredient $cocktailIngredient) {
53+
return [
54+
'name' => $cocktailIngredient->ingredient->name,
55+
'amount' => $cocktailIngredient->amount,
56+
'amount_max' => $cocktailIngredient->amount_max,
57+
'units' => $cocktailIngredient->units,
58+
'optional' => (bool) $cocktailIngredient->optional,
59+
'note' => $cocktailIngredient->note,
60+
'is_specified' => (bool) $cocktailIngredient->is_specified,
61+
'substitutes' => $cocktailIngredient->substitutes->map(function (CocktailIngredientSubstitute $substitute) {
62+
return [
63+
'name' => $substitute->ingredient->name,
64+
'amount' => $substitute->amount,
65+
'amount_max' => $substitute->amount_max,
66+
'units' => $substitute->units,
67+
];
68+
})->toArray(),
69+
];
70+
}),
71+
'created_at' => $this->created_at->toAtomString(),
72+
'updated_at' => $this->updated_at?->toAtomString(),
73+
'abv' => $this->abv,
74+
'volume_ml' => $this->when($this->relationLoaded('ingredients'), fn () => $this->getVolume()),
75+
'alcohol_units' => $this->when($this->relationLoaded('method'), fn () => $this->getAlcoholUnits()),
76+
'calories' => $this->when($this->relationLoaded('method'), fn () => $this->getCalories()),
77+
'year' => $this->year,
78+
];
79+
}
80+
}

app/Models/Bar.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,9 @@ public function getShelfCocktailsOnce(): array
199199
)->values()->toArray();
200200
});
201201
}
202+
203+
public function isPublic(): bool
204+
{
205+
return (bool) $this->is_public;
206+
}
202207
}

app/OpenAPI/Schemas/BarRequest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ class BarRequest
2828
#[OAT\Property(property: 'default_currency', example: 'EUR', description: 'ISO 4217 format of currency. Used only as a setting for client apps.')]
2929
public ?string $defaultCurrency = null;
3030
#[OAT\Property(property: 'enable_invites', description: 'Enable users with invite code to join this bar. Default `false`.')]
31-
public bool $invitesEnabled = true;
31+
public bool $invitesEnabled = false;
3232
#[OAT\Property(description: 'List of data that the bar will start with. Cocktails cannot be imported without ingredients.')]
3333
public ?BarOptionsEnum $options;
3434
/** @var array<int> */
3535
#[OAT\Property(items: new OAT\Items(type: 'integer'), description: 'Existing image ids')]
3636
public array $images = [];
37+
#[OAT\Property(property: 'is_public', description: 'Allow public access to bar recipes. Default `false`.')]
38+
public bool $isPublic = false;
3739

3840
public static function fromLaravelRequest(Request $request): self
3941
{
@@ -46,6 +48,7 @@ public static function fromLaravelRequest(Request $request): self
4648
$result->subtitle = $request->input('subtitle');
4749
$result->description = $request->input('description');
4850
$result->invitesEnabled = $inviteEnabled;
51+
$result->isPublic = $request->boolean('is_public', false);
4952
if ($request->input('slug')) {
5053
$result->slug = Str::slug($request->input('slug'));
5154
}
@@ -70,6 +73,7 @@ public function toLaravelModel(?Bar $model = null): Bar
7073
$bar->name = $this->name;
7174
$bar->subtitle = $this->subtitle;
7275
$bar->description = $this->description;
76+
$bar->is_public = $this->isPublic;
7377

7478
if ($this->invitesEnabled && $bar->invite_code === null) {
7579
$bar->invite_code = (string) new Ulid();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('bars', function (Blueprint $table) {
15+
$table->boolean('is_public')->default(false);
16+
});
17+
}
18+
19+
/**
20+
* Reverse the migrations.
21+
*/
22+
public function down(): void
23+
{
24+
Schema::table('bars', function (Blueprint $table) {
25+
$table->dropColumn('is_public');
26+
});
27+
}
28+
};

0 commit comments

Comments
 (0)