icp2014: pimp dein apigility
DESCRIPTION
Mit Apigility kann jeder Entwickler per Administrationsoberfläche einen REST oder RPC Web Service erstellen und betreiben. Apigility unterstützt zahlreiche Features wie HAL-JSON, Fehlerbehandlung, Versionierung, Authentifizierung und Dokumentation. Dies klappt prima für einfach strukturierte APIs. Doch können auch komplexere Strukturen umgesetzt werden oder stößt man hierbei schnell an die Grenzen? Dieser Vortrag zeigt Ihnen, wann Sie selber Hand an den generierten Code anlegen müssen und warum der Einsatz von Apigility auch bei komplexeren Services viel Sinn macht.TRANSCRIPT
![Page 1: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/1.jpg)
PIMP DEIN APIGILITYPIMP DEIN APIGILITYKomplexere Strukturen mit Apigility
1 / 46
![Page 2: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/2.jpg)
Über michÜber mich
2 / 46www.RalfEggert.dewww.RalfEggert.de
![Page 3: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/3.jpg)
[B00]
PrologProlog
3 / 46
![Page 4: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/4.jpg)
[B01]
4 / 46
Komplexere Komplexere APIs mit APIs mit Apigility?Apigility?
![Page 5: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/5.jpg)
[B02]
5 / 46
Geht auch!Geht auch!
![Page 6: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/6.jpg)
DatenbankmodellDatenbankmodell
6 / 46
![Page 7: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/7.jpg)
[B03]
ApigilityApigilityIn a NutshellIn a Nutshell
7 / 46
![Page 8: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/8.jpg)
Restful Web ServicesRestful Web Services
CLIENT
Web Browser
PHP
Javascript
RUBY
PYTHON
REST SERVER
/api/user/123
/api/user
/api/user
/api/user/123
/api/user/123
USER DOMAIN
getUserEntity()
getUserCollection()
addUserEntity()
updateUserEntity()
deleteUserEntity()
GET Request
JSON Response
GET Request
JSON Response
POST Request
JSON Response
PUT Request
JSON Response
DELETE Request
JSON Response
Integer
UserEntity
void
UserCollection
Array
Boolean
Integer, Array
Boolean
Integer
Boolean
8 / 46
![Page 9: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/9.jpg)
RPC Web ServicesRPC Web ServicesLocalCLIENT
javascript
RPCClient
JSON
Method:getUserParams:id
USER DOMAIN
getUserEntity()
GET Request
JSON Result
Integer
UserEntity
RPCServer
/json-rpc.php
Remote Call
JSON Result
javascriptJSON
Method:addUserParams:name
addUserEntity()POST Request
JSON Result
Array
Boolean
/json-rpc.phpRemote Call
JSON Result
javascriptXML
Method:getUserParams:id
getUserEntity()
GET Request
XML Result
Integer
UserEntity
/xml-rpc.php
Remote Call
XML Result
javascriptXML
Method:addUserParams:name
addUserEntity()
POST Request
XML Result
Array
Boolean
/xml-rpc.php
Remote Call
XML Result
9 / 46
![Page 10: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/10.jpg)
VersionierungVersionierung
default Version per URLdefault Version per URL
Version 1 per URLVersion 1 per URL
Version 2 per URLVersion 2 per URL
default Version per Content Negotiationdefault Version per Content Negotiation
Version 1 per Content NegotiationVersion 1 per Content Negotiation
Version 2 per Content NegotiationVersion 2 per Content Negotiation
10 / 46
![Page 11: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/11.jpg)
JSON / HAL / ProblemJSON / HAL / Problem
11 / 46
![Page 12: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/12.jpg)
WEITERE FEATURESWEITERE FEATURES
Datenbank-basiertDatenbank-basiert Code-basiertCode-basiert authentifizierungauthentifizierung
API DokumentationAPI Dokumentation DatenvalidierungDatenvalidierung DeploymentDeployment
12 / 46
[B06][B05][B04]
[B09][B09][B08][B07]
![Page 13: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/13.jpg)
[b10]
EinfacheEinfacheREST-ServicesREST-Services
13 / 46
![Page 14: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/14.jpg)
DatenbankadapterDatenbankadapter
14 / 46
[b11]
![Page 15: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/15.jpg)
REST SERVICE anlegenREST SERVICE anlegen
15 / 46
[b00]
![Page 16: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/16.jpg)
AUTHENTIFIZIERUNGAUTHENTIFIZIERUNG
16 / 46
[b06]
![Page 17: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/17.jpg)
Testen mit POSTMANTesten mit POSTMAN
1717 / / 4646
[b12][b12]
![Page 18: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/18.jpg)
[B13]
FILTERN +FILTERN +SOrtierenSOrtieren
18 / 46
![Page 19: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/19.jpg)
Collection query stringCollection query string
19 / 46
[b00]
FilterFilter SortSort
![Page 20: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/20.jpg)
CodingCoding
20 / 46
[b00]
![Page 21: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/21.jpg)
Users ResourceUsers Resource// module/User/src/User/V2/Rest/Users/UserResource.php
namespace User\V2\Rest\Users;
use Zend\Paginator\Adapter\DbTableGateway;use ZF\Apigility\DbConnectedResource;
class UsersResource extends DbConnectedResource{ public function fetchAll($data = array()) { $where = isset($data['filter']) ? $data['filter'] : array(); $order = isset($data['sort']) ? $data['sort'] : array();
$adapter = new DbTableGateway($this->table, $where, $order);
return new $this->collectionClass($adapter); }}
21 / 46
![Page 22: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/22.jpg)
KonfigurationKonfiguration// module/User/config/module.config.php
return array( 'zf-apigility' => array( 'db-connected' => array( [...],
'User\\V2\\Rest\\Users\\UsersResource' => array( 'adapter_name' => 'MysqlAdapter', 'table_name' => 'users', 'hydrator_name' => 'Zend\\Stdlib\\Hydrator\\ArraySerializable', 'controller_service_name' => 'User\\V2\\Rest\\Users\\Controller', 'entity_identifier_name' => 'id', 'table_service' => 'User\\V2\\Rest\\Users\\UsersResource\\Table', 'resource_class' => 'User\\V2\\Rest\\Users\\UsersResource', ), ), ),);
22 / 46
![Page 23: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/23.jpg)
Testen mit POSTMANTesten mit POSTMAN
2323 / / 4646
[b12][b12]
![Page 24: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/24.jpg)
[B14]
KomplexereKomplexereREST-ServicesREST-Services
24 / 46
![Page 25: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/25.jpg)
REST SERVICE anlegenREST SERVICE anlegen
25 / 46
[b00]
![Page 26: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/26.jpg)
CodingCoding
26 / 46
[b00]
![Page 27: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/27.jpg)
KonfigurationKonfiguration// module/User/config/module.config.php
return array( [...]
'service_manager' => array( 'factories' => array( 'User\\V3\\Rest\\UserProfiles\\UserProfilesResource' => 'User\\V3\\Rest\\UserProfiles\\UserProfilesResourceFactory', 'User\\V3\\Rest\\UserProfiles\\Table\\UserTable' => 'User\\V3\\Rest\\UserProfiles\\Table\\UserTableFactory', 'User\\V3\\Rest\\UserProfiles\\Table\\WebsiteTable' => 'User\\V3\\Rest\\UserProfiles\\Table\\WebsiteTableFactory', ), ),);
27 / 46
![Page 28: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/28.jpg)
User Table IUser Table I// module/User/src/User/V3/Rest/UserProfiles/Table/UserTable.php
namespace User\V3\Rest\UserProfiles\Table;use Zend\Db\Adapter\AdapterInterface;use Zend\Db\ResultSet\ResultSetInterface;use Zend\Db\TableGateway\TableGateway;
class UserTable extends TableGateway{ public function __construct( AdapterInterface $adapter, ResultSetInterface $resultSetPrototype = null ) { $table = 'users';
parent::__construct($table, $adapter, null, $resultSetPrototype); }
public function fetchUserById($id) { $select = $this->getSql()->select(); $select->where->equalTo('id', $id);
return $this->selectWith($select)->current(); }
28 / 46
![Page 29: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/29.jpg)
User Table IIUser Table II [...]
public function fetchContactsById($id) { $select = $this->getSql()->select(); $select->join('user_contacts', 'user_id_2 = id', array()); $select->where->equalTo('user_id_1', $id);
return $this->selectWith($select)->toArray(); }
public function fetchUsers($params) { $select = $this->getSql()->select();
return $this->selectWith($select)->toArray(); }}
29 / 46
![Page 30: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/30.jpg)
User Table IIIUser Table III// module/User/src/User/V3/Rest/UserProfiles/Table/UserTableFactory.php
namespace User\V3\Rest\UserProfiles\Table;
use Zend\Db\ResultSet\ResultSet;use Zend\ServiceManager\FactoryInterface;use Zend\ServiceManager\ServiceLocatorInterface;
class UserTableFactory implements FactoryInterface{ public function createService(ServiceLocatorInterface $serviceLocator) { $dbAdapter = $serviceLocator->get('MysqlAdapter');
$resultSet = new ResultSet(ResultSet::TYPE_ARRAY);
$table = new UserTable($dbAdapter, $resultSet);
return $table; }}
30 / 46
![Page 31: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/31.jpg)
User Profile entityUser Profile entity// module/User/src/User/V3/Rest/UserProfiles/UserProfilesEntity.php
namespace User\V3\Rest\UserProfiles;
class UserProfilesEntity{ protected $id; protected $firstname; protected $lastname; protected $email; protected $contacts; protected $websites;
public function setId($id) {} public function getId() {} public function setFirstname($firstname) {} public function getFirstame() {} public function setLastname($lastname) {} public function getLastame() {} public function setEmail($email) {} public function getEmail() {} public function setContacts(array $contacts) {} public function getContacts() {} public function setWebsites(array $websites) {} public function getWebsites() {}}
31 / 46
![Page 32: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/32.jpg)
User Profile Resource IUser Profile Resource I// module/User/src/User/V3/Rest/UserProfiles/UserProfilesResource.php
namespace User\V3\Rest\UserProfiles;
use User\V3\Rest\UserProfile\Table\UserTable;use User\V3\Rest\UserProfile\Table\WebsiteTable;use ZF\ApiProblem\ApiProblem;use ZF\Rest\AbstractResourceListener;
class UserProfilesResource extends AbstractResourceListener{ protected $userTable; protected $websiteTable;
public function setUserTable($userTable) {} public function getUserTable() {} public function setWebsiteTable($websiteTable) {} public function getWebsiteTable() {}
public function create($data) { return new ApiProblem(405, 'The POST method has not been defined'); }
public function delete($id) {} public function deleteList($data) {}
32 / 46
![Page 33: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/33.jpg)
User Profile Resource IIUser Profile Resource II public function fetch($id) { $user = $this->getUserTable()->fetchUserById($id); if (!$user) { return new ApiProblem(404, 'User profile for id ' . $id . ' not found'); } return $this->addContactsAndWebsites($user); }
public function fetchAll($params = array()) { $users = $this->getUserTable()->fetchUsers($params); if (!$users) { return new ApiProblem(404, 'No user profiles found'); } foreach ($users as $key => $user) { $users[$key] = $this->addContactsAndWebsites($user); } return $users; }
protected function addContactsAndWebsites(array $user) { $user['contacts'] = $this->getUserTable()->fetchContactsById($user['id']); $user['websites'] = $this->getWebsiteTable()->fetchWebsitesById($user['id']); return $user; }}
33 / 46
![Page 34: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/34.jpg)
User Profile Resource IIIUser Profile Resource III// module/User/src/User/V3/Rest/UserProfiles/UserProfilesResourceFactory.php
namespace User\V3\Rest\UserProfiles;
class UserProfilesResourceFactory{ public function __invoke($services) { $userTable = $services->get( 'User\\V2\\Rest\\UserProfile\\Table\\UserTable' ); $websiteTable = $services->get( 'User\\V2\\Rest\\UserProfile\\Table\\WebsiteTable' );
$resource = new UserProfilesResource(); $resource->setUserTable($userTable); $resource->setWebsiteTable($websiteTable);
return $resource; }}
34 / 46
![Page 35: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/35.jpg)
Testen mit POSTMANTesten mit POSTMAN
3535 / / 4646
[b12][b12]
![Page 36: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/36.jpg)
[B13]
Datei UploadDatei Upload
36 / 46
![Page 37: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/37.jpg)
Content-TypeContent-Type
37 / 46
[b00]
Multipart/form-dataMultipart/form-data
![Page 38: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/38.jpg)
Image Field ergänzenImage Field ergänzen
38 / 46
[b00]
![Page 39: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/39.jpg)
CodingCoding
39 / 46
[b00]
![Page 40: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/40.jpg)
Users ResourceUsers Resource// module/User/src/User/V4/Rest/Users/UserResource.php
namespace User\V4\Rest\Users;use ZF\Apigility\DbConnectedResource;
class UsersResource extends DbConnectedResource{ protected function retrieveData($data) { if (null !== $filter) { $data = $this->getInputFilter()->getValues(); } else { $data = (array) $data; }
$image = $this->getInputFilter()->getValue('image'); $imagePath = '/img/uploads/' . md5(serialize($data)) . '.jpg'; $fullPath = APPLICATION_PATH . '/public' . $imagePath;
copy(APPLICATION_PATH . '/' . $image['tmp_name'], $fullPath); unlink(APPLICATION_PATH . '/' . $image['tmp_name']);
$data['image'] = $imagePath;
return $data; }}
40 / 46
![Page 41: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/41.jpg)
Testen mit POSTMANTesten mit POSTMAN
4141 / / 4646
[b12][b12]
![Page 42: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/42.jpg)
[B15]
Apigility ohne Apigility ohne ApigilityApigility
(Fast wie Kochen ohne Blumenkohl)(Fast wie Kochen ohne Blumenkohl)
42 / 46
![Page 43: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/43.jpg)
Modularer AufbauModularer AufbauZf-apigilityZf-apigility Zf-apigility-adminZf-apigility-admin Zf-apigility-documentationZf-apigility-documentation
Zf-apigility-documentation-swaggerZf-apigility-documentation-swagger Zf-apigility-providerZf-apigility-provider Zf-apigility-WelcomeZf-apigility-Welcome
Zf-api-problemZf-api-problem Zf-configurationZf-configuration Zf-content-negotiationZf-content-negotiation
Zf-content-validationZf-content-validation Zf-deployZf-deploy Zf-development-modeZf-development-mode
Zf-halZf-hal Zf-mvc-authZf-mvc-auth Zf-oauth2Zf-oauth2
Zf-restZf-rest Zf-RPCZf-RPC Zf-versioningZf-versioning
43 / 46
![Page 44: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/44.jpg)
[B00]
EPILOGEPILOG
44 / 46
![Page 45: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/45.jpg)
PIMP DEIN APIGILITYPIMP DEIN APIGILITYKomplexere Strukturen mit Apigility
Repository: https://github.com/RalfEggert/ipc2014-apigility
45 / 46
![Page 46: ICP2014: Pimp dein Apigility](https://reader033.vdocuments.pub/reader033/viewer/2022052507/558fcef51a28ab627f8b4677/html5/thumbnails/46.jpg)
BildnachweisBildnachweis[B00] Fotos von Ralf Eggert
[B01] Still here https://www.flickr.com/photos/thenovys/3784261365 von Abe Novy - CC-BY https://creativecommons.org/licenses/by/2.0/
[B02] Young student https://www.flickr.com/photos/audiolucistore/14159712431/ von www.audio-luci-store.it - CC-BY https://creativecommons.org/licenses/by/2.0/
[B03] Acorns https://www.flickr.com/photos/dno1967b/5431273344 von Daniel Oines - CC-BY https://creativecommons.org/licenses/by/2.0/
[B04] Fixing the database https://www.flickr.com/photos/dahlstroms/4140461901 von Håkan Dahlström - CC-BY https://creativecommons.org/licenses/by/2.0/
[B05] Monaco 14pt https://www.flickr.com/photos/polarity/3138680190 von Robert Agthe - CC-BY https://creativecommons.org/licenses/by/2.0/
[B06] RSA Securid Token - Credit Card Style https://www.flickr.com/photos/purpleslog/265657780 von Purple Slog - CC-BY https://creativecommons.org/licenses/by/2.0/
[B07] Shelf of Used Books https://www.flickr.com/photos/thedarkthing/5363586197 von William Ross - CC-BY https://creativecommons.org/licenses/by/2.0/
[B08] Ticket validator at Nice train station https://www.flickr.com/photos/traveleden/3797157077 von Simon - CC-BY https://creativecommons.org/licenses/by/2.0/
[B09] Test Lab - Supermicro Storage https://www.flickr.com/photos/jemimus/8533890844 von Robert - CC-BY https://creativecommons.org/licenses/by/2.0/
[B10] Building blocks https://www.flickr.com/photos/zscheyge/49012397 von Holger Zscheyge - CC-BY https://creativecommons.org/licenses/by/2.0/
[B11] Bluetooth adapter https://www.flickr.com/photos/razor512/8201846881 von Razor512 - CC-BY https://creativecommons.org/licenses/by/2.0/
[B12] Busy Postmen https://www.flickr.com/photos/wheatfields/4253690499 von Christian Guthier - CC-BY https://creativecommons.org/licenses/by/2.0/
[B13] Filters for black and white film https://www.flickr.com/photos/aslakr/96723190 von Aslak Raanes - CC-BY https://creativecommons.org/licenses/by/2.0/
[B14] Cable closet https://www.flickr.com/photos/adrian_s/8271860 von Adrian Sampson - CC-BY https://creativecommons.org/licenses/by/2.0/
[B15] Cauliflower https://www.flickr.com/photos/calliope/54833239 von Liz West - CC-BY https://creativecommons.org/licenses/by/2.0/
Alle weiteren Screenshots wurden von Ralf Eggert erstellt.
46 / 46