Skip to content

Commit 08dd5a6

Browse files
committed
Add open graph service
1 parent 9d09b72 commit 08dd5a6

37 files changed

+2653
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
/.phpunit.result.cache
99

10+
/.claude/*.local.json
11+
1012
# Symfony CLI https://symfony.com/doc/current/setup/symfony_server.html#different-php-settings-per-project
1113
/.php-version
1214
/php.ini

src/DependencyInjection/SetonoSyliusSEOExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public function prepend(ContainerBuilder $container): void
8080
'setono_sylius_seo_json_ld' => [
8181
'template' => '@SetonoSyliusSEOPlugin/json_ld.html.twig',
8282
],
83+
'setono_sylius_seo_open_graph' => [
84+
'template' => '@SetonoSyliusSEOPlugin/open_graph.html.twig',
85+
],
8386
],
8487
],
8588
],

src/OpenGraph/OpenGraph.php

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Setono\SyliusSEOPlugin\OpenGraph;
6+
7+
use Setono\SyliusSEOPlugin\OpenGraph\Property\Audio;
8+
use Setono\SyliusSEOPlugin\OpenGraph\Property\Image;
9+
use Setono\SyliusSEOPlugin\OpenGraph\Property\Video;
10+
use Setono\SyliusSEOPlugin\OpenGraph\Type\TypeInterface;
11+
use Setono\SyliusSEOPlugin\OpenGraph\Type\Website;
12+
13+
/**
14+
* @see https://ogp.me/
15+
*/
16+
final class OpenGraph
17+
{
18+
private ?string $title = null;
19+
20+
private TypeInterface $type;
21+
22+
private ?string $url = null;
23+
24+
private ?string $description = null;
25+
26+
private ?string $determiner = null;
27+
28+
private ?string $locale = null;
29+
30+
/** @var list<string> */
31+
private array $localeAlternates = [];
32+
33+
private ?string $siteName = null;
34+
35+
/** @var list<Image> */
36+
private array $images = [];
37+
38+
/** @var list<Video> */
39+
private array $videos = [];
40+
41+
/** @var list<Audio> */
42+
private array $audios = [];
43+
44+
public function __construct()
45+
{
46+
$this->type = new Website();
47+
}
48+
49+
public function title(string $title): self
50+
{
51+
$this->title = $title;
52+
53+
return $this;
54+
}
55+
56+
public function getTitle(): ?string
57+
{
58+
return $this->title;
59+
}
60+
61+
public function type(TypeInterface $type): self
62+
{
63+
$this->type = $type;
64+
65+
return $this;
66+
}
67+
68+
public function getType(): TypeInterface
69+
{
70+
return $this->type;
71+
}
72+
73+
public function url(string $url): self
74+
{
75+
$this->url = $url;
76+
77+
return $this;
78+
}
79+
80+
public function getUrl(): ?string
81+
{
82+
return $this->url;
83+
}
84+
85+
public function description(string $description): self
86+
{
87+
$this->description = $description;
88+
89+
return $this;
90+
}
91+
92+
public function getDescription(): ?string
93+
{
94+
return $this->description;
95+
}
96+
97+
/**
98+
* Set the word that appears before this object's title in a sentence.
99+
* Should be one of: "a", "an", "the", "", "auto".
100+
*/
101+
public function determiner(string $determiner): self
102+
{
103+
$this->determiner = $determiner;
104+
105+
return $this;
106+
}
107+
108+
public function getDeterminer(): ?string
109+
{
110+
return $this->determiner;
111+
}
112+
113+
/**
114+
* Set the locale these tags are marked up in.
115+
* Format: language_TERRITORY (e.g., "en_US", "fr_FR").
116+
*/
117+
public function locale(string $locale): self
118+
{
119+
$this->locale = $locale;
120+
121+
return $this;
122+
}
123+
124+
public function getLocale(): ?string
125+
{
126+
return $this->locale;
127+
}
128+
129+
public function localeAlternate(string $locale): self
130+
{
131+
$this->localeAlternates[] = $locale;
132+
133+
return $this;
134+
}
135+
136+
/**
137+
* @return list<string>
138+
*/
139+
public function getLocaleAlternates(): array
140+
{
141+
return $this->localeAlternates;
142+
}
143+
144+
public function siteName(string $siteName): self
145+
{
146+
$this->siteName = $siteName;
147+
148+
return $this;
149+
}
150+
151+
public function getSiteName(): ?string
152+
{
153+
return $this->siteName;
154+
}
155+
156+
/**
157+
* Add an image to represent your object.
158+
*
159+
* @param Image|string $image Image object or URL
160+
*/
161+
public function image(Image|string $image): self
162+
{
163+
if (is_string($image)) {
164+
$image = new Image($image);
165+
}
166+
167+
$this->images[] = $image;
168+
169+
return $this;
170+
}
171+
172+
/**
173+
* @return list<Image>
174+
*/
175+
public function getImages(): array
176+
{
177+
return $this->images;
178+
}
179+
180+
/**
181+
* Add a video to complement your object.
182+
*
183+
* @param Video|string $video Video object or URL
184+
*/
185+
public function video(Video|string $video): self
186+
{
187+
if (is_string($video)) {
188+
$video = new Video($video);
189+
}
190+
191+
$this->videos[] = $video;
192+
193+
return $this;
194+
}
195+
196+
/**
197+
* @return list<Video>
198+
*/
199+
public function getVideos(): array
200+
{
201+
return $this->videos;
202+
}
203+
204+
/**
205+
* Add an audio file to complement your object.
206+
*
207+
* @param Audio|string $audio Audio object or URL
208+
*/
209+
public function audio(Audio|string $audio): self
210+
{
211+
if (is_string($audio)) {
212+
$audio = new Audio($audio);
213+
}
214+
215+
$this->audios[] = $audio;
216+
217+
return $this;
218+
}
219+
220+
/**
221+
* @return list<Audio>
222+
*/
223+
public function getAudios(): array
224+
{
225+
return $this->audios;
226+
}
227+
228+
/**
229+
* Convert the Open Graph data to an array of property => value pairs.
230+
*
231+
* @return array<string, scalar|list<scalar>>
232+
*/
233+
public function toArray(): array
234+
{
235+
$data = [];
236+
237+
if (null !== $this->title) {
238+
$data['og:title'] = $this->title;
239+
}
240+
241+
$data['og:type'] = $this->type->getType();
242+
243+
if (null !== $this->url) {
244+
$data['og:url'] = $this->url;
245+
}
246+
247+
if (null !== $this->description) {
248+
$data['og:description'] = $this->description;
249+
}
250+
251+
if (null !== $this->determiner) {
252+
$data['og:determiner'] = $this->determiner;
253+
}
254+
255+
if (null !== $this->locale) {
256+
$data['og:locale'] = $this->locale;
257+
}
258+
259+
if ([] !== $this->localeAlternates) {
260+
$data['og:locale:alternate'] = $this->localeAlternates;
261+
}
262+
263+
if (null !== $this->siteName) {
264+
$data['og:site_name'] = $this->siteName;
265+
}
266+
267+
foreach ($this->images as $image) {
268+
foreach ($image->toArray() as $property => $value) {
269+
$data[$property] = array_key_exists($property, $data)
270+
? $this->mergeArrayValue($data[$property], $value)
271+
: $value;
272+
}
273+
}
274+
275+
foreach ($this->videos as $video) {
276+
foreach ($video->toArray() as $property => $value) {
277+
$data[$property] = array_key_exists($property, $data)
278+
? $this->mergeArrayValue($data[$property], $value)
279+
: $value;
280+
}
281+
}
282+
283+
foreach ($this->audios as $audio) {
284+
foreach ($audio->toArray() as $property => $value) {
285+
$data[$property] = array_key_exists($property, $data)
286+
? $this->mergeArrayValue($data[$property], $value)
287+
: $value;
288+
}
289+
}
290+
291+
return array_merge($data, $this->type->getProperties());
292+
}
293+
294+
/**
295+
* @param scalar|list<scalar> $existing
296+
*
297+
* @return list<scalar>
298+
*/
299+
private function mergeArrayValue(array|bool|float|int|string $existing, bool|float|int|string $value): array
300+
{
301+
if (is_array($existing)) {
302+
$existing[] = $value;
303+
304+
return $existing;
305+
}
306+
307+
return [$existing, $value];
308+
}
309+
310+
/**
311+
* Render the Open Graph data as HTML meta tags.
312+
*/
313+
public function toHtml(): string
314+
{
315+
$html = [];
316+
317+
foreach ($this->toArray() as $property => $value) {
318+
if (is_array($value)) {
319+
foreach ($value as $item) {
320+
$html[] = $this->renderMetaTag($property, $item);
321+
}
322+
} else {
323+
$html[] = $this->renderMetaTag($property, $value);
324+
}
325+
}
326+
327+
return implode("\n", $html);
328+
}
329+
330+
private function renderMetaTag(string $property, bool|float|int|string $content): string
331+
{
332+
return sprintf(
333+
'<meta property="%s" content="%s">',
334+
htmlspecialchars($property, \ENT_QUOTES | \ENT_HTML5, 'UTF-8'),
335+
htmlspecialchars((string) $content, \ENT_QUOTES | \ENT_HTML5, 'UTF-8'),
336+
);
337+
}
338+
}

0 commit comments

Comments
 (0)