Skip to content

Commit 85a48e0

Browse files
DeryabinSergeyDeriabin Sergey
andauthored
Open graph protocol (#257)
* Add Open Grap Protocol implementation * Add image array for multiple tags allowed. Some cosmetics * Cosmetic fix. Add tests * Fix copyright year * Fix class comment Co-authored-by: Deriabin Sergey <[email protected]>
1 parent 7eba2e8 commit 85a48e0

27 files changed

+3555
-0
lines changed

src/Main/Markup/OGP/OpenGraph.php

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
<?php
2+
/***************************************************************************
3+
* Copyright (C) 2021 by Sergei V. Deriabin *
4+
* *
5+
* This program is free software; you can redistribute it and/or modify *
6+
* it under the terms of the GNU Lesser General Public License as *
7+
* published by the Free Software Foundation; either version 3 of the *
8+
* License, or (at your option) any later version. *
9+
* *
10+
***************************************************************************/
11+
12+
namespace OnPHP\Main\Markup\OGP;
13+
14+
use OnPHP\Core\Base\Assert;
15+
use OnPHP\Core\Exception\WrongArgumentException;
16+
use OnPHP\Main\Markup\Html\HtmlAssembler;
17+
use OnPHP\Main\Markup\Html\SgmlOpenTag;
18+
19+
/**
20+
* The Open Graph protocol
21+
* @see https://ogp.me/
22+
* @see https://developers.facebook.com/docs/sharing/webmasters
23+
* @see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards
24+
*
25+
* Validators:
26+
* @see https://cards-dev.twitter.com/validator
27+
* @see https://www.linkedin.com/post-inspector/inspect/
28+
* @see https://developers.facebook.com/tools/debug/
29+
*
30+
* @ingroup Markup
31+
* @ingroup OGP
32+
*/
33+
class OpenGraph
34+
{
35+
const OGP_NAMESPACE = ['og', 'https://ogp.me/ns#'] ;
36+
const FB_NAMESPACE = ['fb', 'https://ogp.me/ns/fb#'];
37+
38+
const ALLOWED_DATERMINE = ['a', 'an', 'the', 'auto'];
39+
40+
/**
41+
* The title of your object as it should appear within the graph, e.g., "The Rock".
42+
* @var ?string
43+
*/
44+
protected ?string $title = null;
45+
/**
46+
* A one to two sentence description of your object.
47+
* @var ?string
48+
*/
49+
protected ?string $description = null;
50+
/**
51+
* The word that appears before this object's title in a sentence.
52+
* An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your
53+
* data should chose between "a" or "an". Default is "" (blank).
54+
* @var string
55+
*/
56+
protected string $determiner = '';
57+
/**
58+
* The locale these tags are marked up in.
59+
* Of the format language_TERRITORY. Default is en_US.
60+
* @var string
61+
*/
62+
protected string $locale = 'en_US';
63+
/**
64+
* An array of other locales this page is available in.
65+
* @var string[]
66+
*/
67+
protected array $localeAlternates = [];
68+
/**
69+
* If your object is part of a larger web site, the name which
70+
* should be displayed for the overall site. e.g., "IMDb".
71+
* @var ?string
72+
*/
73+
protected ?string $siteName = null;
74+
/**
75+
* The type of your object, e.g., object OpenGraphVideo
76+
* @var ?OpenGraphObject
77+
*/
78+
protected ?OpenGraphObject $type = null;
79+
/**
80+
* An image which should represent your object within the graph.
81+
* @var OpenGraphImage[]
82+
*/
83+
protected array $image = [];
84+
/**
85+
* @var ?OpenGraphVideo
86+
*/
87+
protected ?OpenGraphVideo $video = null;
88+
/**
89+
* Object OpenGraphAudio an audio file to accompany this object.
90+
* @var ?OpenGraphAudio
91+
*/
92+
protected ?OpenGraphAudio $audio = null;
93+
/**
94+
* The canonical URL of your object that will be used as its permanent
95+
* ID in the graph, e.g., "https://www.imdb.com/title/tt0117500/".
96+
* @var ?string
97+
*/
98+
protected ?string $url = null;
99+
/**
100+
* @var ?string
101+
*/
102+
protected ?string $vkImage = null;
103+
/**
104+
* @var ?string
105+
*/
106+
protected ?string $appId = null;
107+
/**
108+
* @var ?OpenGraphTwitterCard
109+
*/
110+
protected ?OpenGraphTwitterCard $twitterCard = null;
111+
112+
/**
113+
* @return static
114+
*/
115+
public static function create(): static
116+
{
117+
return new static;
118+
}
119+
120+
/**
121+
* @param string $title
122+
* @return static
123+
*/
124+
public function setTitle(string $title): static
125+
{
126+
$this->title = $title;
127+
128+
return $this;
129+
}
130+
131+
/**
132+
* @param string $description
133+
* @return static
134+
*/
135+
public function setDescription(string $description): static
136+
{
137+
$this->description = $description;
138+
139+
return $this;
140+
}
141+
142+
/**
143+
* @param string $daterminer
144+
* @return static
145+
* @throws WrongArgumentException
146+
*/
147+
public function setDaterminer(string $daterminer): static
148+
{
149+
Assert::isTrue(
150+
empty($daterminer) || in_array($daterminer, self::ALLOWED_DATERMINE),
151+
'Only empty value or `a`, `an`, `the`, `auto` allowed'
152+
);
153+
$this->determiner = $daterminer;
154+
155+
return $this;
156+
}
157+
158+
/**
159+
* @param string $locale
160+
* @return static
161+
* @throws WrongArgumentException
162+
*/
163+
public function setLocale(string $locale): static
164+
{
165+
Assert::isTrue(
166+
preg_match('/^[a-z]{2}_[A-Z]{2}$/iu', $locale) == 1,
167+
'wrong locale format'
168+
);
169+
$this->locale = $locale;
170+
171+
return $this;
172+
}
173+
174+
/**
175+
* @param string $locale
176+
* @return static
177+
* @throws WrongArgumentException
178+
*/
179+
public function setLocaleAlternates(string $locale): static
180+
{
181+
Assert::isTrue(
182+
preg_match('/^[a-z]{2}_[A-Z]{2}$/iu', $locale) == 1,
183+
'wrong locale format'
184+
);
185+
$this->localeAlternates[] = $locale;
186+
187+
return $this;
188+
}
189+
190+
/**
191+
* @param string $siteName
192+
* @return static
193+
*/
194+
public function setSiteName(string $siteName): static
195+
{
196+
$this->siteName = $siteName;
197+
198+
return $this;
199+
}
200+
201+
/**
202+
* @param OpenGraphObject $type
203+
* @return static
204+
*/
205+
public function setType(OpenGraphObject $type): static
206+
{
207+
$this->type = $type;
208+
209+
return $this;
210+
}
211+
212+
/**
213+
* @param OpenGraphImage $image
214+
* @return static
215+
*/
216+
public function setImage(OpenGraphImage $image): static
217+
{
218+
$this->image[] = $image;
219+
220+
return $this;
221+
}
222+
223+
/**
224+
* @param mixed $appId
225+
* @return static
226+
*/
227+
public function setAppId(mixed $appId): static
228+
{
229+
$this->appId = (string)$appId;
230+
231+
return $this;
232+
}
233+
234+
/**
235+
* @param OpenGraphVideo $video
236+
* @return static
237+
*/
238+
public function setVideo(OpenGraphVideo $video): static
239+
{
240+
$this->video = $video;
241+
242+
return $this;
243+
}
244+
245+
/**
246+
* @param OpenGraphTwitterCard $twitterCard
247+
* @return static
248+
*/
249+
public function setTwitterCart(OpenGraphTwitterCard $twitterCard): static
250+
{
251+
$this->twitterCard = $twitterCard;
252+
253+
return $this;
254+
}
255+
256+
/**
257+
* @param OpenGraphAudio $audio
258+
* @return static
259+
*/
260+
public function setAudio(OpenGraphAudio $audio): static
261+
{
262+
$this->audio = $audio;
263+
264+
return $this;
265+
}
266+
267+
/**
268+
* @param string $url
269+
* @return static
270+
*/
271+
public function setUrl(string $url): static
272+
{
273+
$this->url = $url;
274+
275+
return $this;
276+
}
277+
278+
/**
279+
* Minimal image size - 160 x 160 px. Recommend greater than 510 x 228 px.
280+
* @see https://vk.com/dev/publications
281+
* @param string $vkImage
282+
* @return static
283+
*/
284+
public function setVkImage(string $vkImage): static
285+
{
286+
$this->vkImage = $vkImage;
287+
288+
return $this;
289+
}
290+
291+
/**
292+
* @param bool $full
293+
* @return string
294+
* @throws WrongArgumentException
295+
*/
296+
public function getPrefix(bool $full = true): string
297+
{
298+
Assert::isNotEmpty($this->type, 'type is required');
299+
300+
$prefix = [
301+
self::OGP_NAMESPACE[0] . ': ' . self::OGP_NAMESPACE[1],
302+
$this->type->getNamespace() . ': ' . $this->type->getType()->getNamespace(),
303+
];
304+
if (!empty($this->appId)) {
305+
$prefix[] = self::FB_NAMESPACE[0] . ': ' . self::FB_NAMESPACE[1];
306+
}
307+
308+
return
309+
($full ? 'prefix="' : '')
310+
. implode(" ", $prefix)
311+
. ($full ? '"' : '');
312+
}
313+
314+
/**
315+
* @return string
316+
* @throws WrongArgumentException
317+
*/
318+
public function dump(): string
319+
{
320+
Assert::isNotEmpty($this->title, 'title is required');
321+
Assert::isNotEmpty($this->type, 'type is required');
322+
Assert::isNotEmpty($this->url, 'url is required');
323+
Assert::isNotEmpty($this->image, 'image is required');
324+
Assert::isNotEmpty($this->description, 'description is required');
325+
326+
return
327+
(new HtmlAssembler(
328+
array_map(
329+
function ($item) {
330+
return (new SgmlOpenTag())->setId('meta')->setEmpty(true)
331+
->setAttribute('property', $item[0])
332+
->setAttribute('content', $item[1]);
333+
},
334+
$this->getList()
335+
)
336+
)
337+
)->getHtml();
338+
}
339+
340+
/**
341+
* @return array
342+
* @throws WrongArgumentException
343+
*/
344+
protected function getList(): array
345+
{
346+
return array_merge([
347+
['og:title', $this->title],
348+
['og:url', $this->url],
349+
['og:type', $this->type->getType()->getName()],
350+
['og:locale', $this->locale]
351+
],
352+
array_map(
353+
function ($item) { return ['og:locale:alternate', $item]; },
354+
$this->localeAlternates
355+
),
356+
array_reduce(
357+
$this->image,
358+
function ($result, OpenGraphImage $image) {
359+
return array_merge($result, $image->getList());
360+
}, []
361+
),
362+
$this->audio?->getList() ?? [],
363+
$this->video?->getList() ?? [],
364+
empty($this->description) ? [] : [ ['og:description', $this->description] ],
365+
empty($this->determiner) ? [] : [ ['og:determiner', $this->description] ],
366+
empty($this->siteName) ? [] : [ ['og:site_name', $this->description] ],
367+
empty($this->appId) ? [] : [ ['fb:app_id', $this->appId] ],
368+
empty($this->vkImage) ? [] : [ ['vk:image', $this->vkImage] ],
369+
$this->type->getList(),
370+
empty($this->twitterCard) ? [] : $this->twitterCard->getList()
371+
);
372+
}
373+
}

0 commit comments

Comments
 (0)