Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Move this file to the root directory of your project or merge it with the existing one
SHOW_GENERAL_GREETING=true
OEEM_SHOP_NAME='OXID eShop from env file'
API_JWT_SECRET='change-this-to-a-secure-random-string'
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,31 @@ The repository contains examples of following cases and more:
* [Access via DI container](src/Greeting/services.yaml)
* Note: After updating environment variables, you must clear the cache for changes to take effect.

* [API Entrypoint examples](src/ApiEntrypoint) — four endpoints demonstrating the four authentication models
* **Public endpoint** — [ProductInfo](src/ApiEntrypoint/ProductInfo/Controller/ProductInfoApiController.php): `GET /api/product-info`
* No authentication required
* Returns JSON with active product count of current shop and translated greeting message
* Demonstrates:
* `#[Route]` attribute
* service injection
* shop/language-aware database view via `ShopAdapterInterface::generateDatabaseViewName()`
* translation via `ShopAdapterInterface`
* **JWT-protected endpoint** — [CustomerGroup](src/ApiEntrypoint/CustomerGroup/Controller/CustomerGroupApiController.php): `GET /api/customer-groups`
* Requires `#[IsGranted('ROLE_ADMIN')]` — admin JWT token via `Authorization: Bearer`
* Requires `oxid-esales/jwt-authentication-component` — obtain a token via `POST /api/login` (see [JWT component README](https://github.com/OXID-eSales/jwt-authentication-component#login) for details)
* Returns customer counts per user group (sensitive business data)
* **Frontend session endpoint** — [UserInfo](src/ApiEntrypoint/UserInfo/Controller/UserInfoApiController.php): `GET /api/user-info`
* Requires `#[SessionUser]` — active frontend session (`sid` cookie)
* Requires `oxid-esales/session-authentication-component`
* Returns logged-in user's first name and greeting controller URL
* Demonstrates storefront AJAX use case: [header button](views/twig/extensions/themes/default/layout/header.html.twig) fetches endpoint and shows personalized greeting link
* **Admin session endpoint** — [AdminInfo](src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php): `GET /api/admin-info`
* Requires `#[AdminSessionUser(roles: ['ROLE_ADMIN'])]` — active admin session (`admin_sid` cookie)
* Requires `oxid-esales/session-authentication-component`
* Returns translated greeting with admin email (e.g. "Hello, Admin admin@example.com")
* Demonstrates admin AJAX use case: [admin header greeting](views/twig/extensions/themes/admin_twig/include/header_links.html.twig)
* Each example follows a layered structure with interfaces at every boundary (Controller → Service → Repository → DTO where applicable)

**HINTS**:
* Only extend the shop core if there is no other way like listen and handle shop events,
decorate/replace some DI service.
Expand Down
14 changes: 13 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"require": {
"php": "^8.3",
"symfony/filesystem": "^6.4",
"ddoe/wysiwyg-editor-module": "dev-b-7.5.x"
"ddoe/wysiwyg-editor-module": "dev-b-7.5.x",
"oxid-esales/jwt-authentication-component": "dev-b-7.5.x",
"oxid-esales/session-authentication-component": "dev-b-7.5.x"
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand Down Expand Up @@ -88,5 +90,15 @@
"oxid-esales/oxideshop-composer-plugin": true,
"oxid-esales/oxideshop-unified-namespace-generator": true
}
},
"repositories": {
"oxid-esales/jwt-authentication-component": {
"type": "git",
"url": "https://github.com/OXID-eSales/jwt-authentication-component"
},
"oxid-esales/session-authentication-component": {
"type": "git",
"url": "https://github.com/OXID-eSales/session-authentication-component"
}
}
}
4 changes: 0 additions & 4 deletions recipes/setup-development.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ make up

docker compose exec php composer update --no-interaction

perl -pi\
-e 'print "SetEnvIf Authorization \"(.*)\" HTTP_AUTHORIZATION=\$1\n\n" if $. == 1'\
source/source/.htaccess

$SCRIPT_PATH/parts/shared/setup_database.sh --no-demodata

docker compose exec -T php vendor/bin/oe-console oe:module:install ./
Expand Down
1 change: 1 addition & 0 deletions services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ imports:
- { resource: src/Logging/services.yaml }
- { resource: src/ProductVote/services.yaml }
- { resource: src/Tracker/services.yaml }
- { resource: src/ApiEntrypoint/services.yaml }

services:

Expand Down
42 changes: 42 additions & 0 deletions src/ApiEntrypoint/AdminInfo/Controller/AdminInfoApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Controller;

use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service\AdminInfoServiceInterface;
use OxidEsales\SessionAuthComponent\Security\Attribute\AdminSessionUser;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\User\UserInterface;

readonly class AdminInfoApiController
{
public function __construct(
private AdminInfoServiceInterface $adminInfoService,
) {
}

#[Route('/api/admin-info', methods: ['GET'])]
#[AdminSessionUser(roles: ['ROLE_ADMIN'])]
public function getAdminInfo(Request $request): JsonResponse
{
/** @var UserInterface $user */
$user = $request->attributes->get('_user');

$adminInfo = $this->adminInfoService->getAdminInfo(
$user->getUserIdentifier()
);

return new JsonResponse([
'email' => $adminInfo->getEmail(),
'greeting' => $adminInfo->getGreeting(),
]);
}
}
29 changes: 29 additions & 0 deletions src/ApiEntrypoint/AdminInfo/DTO/AdminInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO;

readonly class AdminInfo implements AdminInfoInterface
{
public function __construct(
private string $email,
private string $greeting,
) {
}

public function getEmail(): string
{
return $this->email;
}

public function getGreeting(): string
{
return $this->greeting;
}
}
17 changes: 17 additions & 0 deletions src/ApiEntrypoint/AdminInfo/DTO/AdminInfoInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO;

interface AdminInfoInterface
{
public function getEmail(): string;

public function getGreeting(): string;
}
35 changes: 35 additions & 0 deletions src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service;

use OxidEsales\EshopCommunity\Internal\Transition\Adapter\ShopAdapterInterface;
use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfo;
use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfoInterface;
use OxidEsales\ExamplesModule\Core\Module as ModuleCore;

readonly class AdminInfoService implements AdminInfoServiceInterface
{
public function __construct(
private ShopAdapterInterface $shopAdapter,
) {
}

public function getAdminInfo(string $username): AdminInfoInterface
{
$greetingPattern = $this->shopAdapter->translateString(
ModuleCore::ADMIN_HELLO_LANGUAGE_CONST
);

return new AdminInfo(
email: $username,
greeting: sprintf($greetingPattern, $username),
);
}
}
17 changes: 17 additions & 0 deletions src/ApiEntrypoint/AdminInfo/Service/AdminInfoServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service;

use OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\DTO\AdminInfoInterface;

interface AdminInfoServiceInterface
{
public function getAdminInfo(string $username): AdminInfoInterface;
}
10 changes: 10 additions & 0 deletions src/ApiEntrypoint/AdminInfo/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
_defaults:
public: false
autowire: true

OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service\AdminInfoServiceInterface:
class: OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Service\AdminInfoService

OxidEsales\ExamplesModule\ApiEntrypoint\AdminInfo\Controller\AdminInfoApiController:
public: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Controller;

use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service\CustomerGroupServiceInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

readonly class CustomerGroupApiController
{
public function __construct(
private CustomerGroupServiceInterface $customerGroupService,
) {
}

#[Route('/api/customer-groups', methods: ['GET'])]
#[IsGranted('ROLE_ADMIN')]
public function getCustomerGroups(): JsonResponse
{
$groups = $this->customerGroupService->getCustomerGroupCounts();

$groupsData = [];
foreach ($groups as $group) {
$groupsData[] = [
'groupId' => $group->getGroupId(),
'title' => $group->getTitle(),
'count' => $group->getCount(),
];
}

return new JsonResponse([
'customerGroups' => $groupsData,
'total' => $this->customerGroupService->getTotalCustomerCount(),
]);
}
}
35 changes: 35 additions & 0 deletions src/ApiEntrypoint/CustomerGroup/DTO/CustomerGroupCount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO;

readonly class CustomerGroupCount implements CustomerGroupCountInterface
{
public function __construct(
private string $groupId,
private string $title,
private int $count,
) {
}

public function getGroupId(): string
{
return $this->groupId;
}

public function getTitle(): string
{
return $this->title;
}

public function getCount(): int
{
return $this->count;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO;

interface CustomerGroupCountInterface
{
public function getGroupId(): string;

public function getTitle(): string;

public function getCount(): int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* Copyright © . All rights reserved.
* See LICENSE file for license details.
*/

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Infrastructure;

use Doctrine\DBAL\Result;
use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface;
use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DTO\CustomerGroupCount;

readonly class CustomerGroupRepository implements CustomerGroupRepositoryInterface
{
public function __construct(
private QueryBuilderFactoryInterface $queryBuilderFactory,
) {
}

/** @return list<CustomerGroupCount> */
public function getCustomerGroupCounts(): array
{
$queryBuilder = $this->queryBuilderFactory->create();
$queryBuilder
->select([
'g.oxid AS groupId',
'g.oxtitle AS title',
'COUNT(u2g.oxid) AS customerCount',
])
->from('oxgroups', 'g')
->leftJoin(
'g',
'oxobject2group',
'u2g',
'g.oxid = u2g.oxgroupsid'
)
->where('g.oxactive = 1')
->groupBy('g.oxid, g.oxtitle')
->orderBy('g.oxtitle', 'ASC');

/** @var Result $result */
$result = $queryBuilder->execute();
$rows = $result->fetchAllAssociative();

$counts = [];
foreach ($rows as $row) {
$counts[] = new CustomerGroupCount(
groupId: $row['groupId'],
title: $row['title'],
count: (int) $row['customerCount'],
);
}

return $counts;
}
}
Loading
Loading