Skip to content
Open
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'
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,29 @@ 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, DAO pattern, and 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`
* Returns customer counts per user group (sensitive business data)
* Demonstrates readonly DTO ([CustomerGroupCount](src/ApiEntrypoint/CustomerGroup/DataObject/CustomerGroupCount.php)), LEFT JOIN in DAO
* 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)
* **Frontend session endpoint** — [UserInfo](src/ApiEntrypoint/UserInfo/Controller/UserInfoApiController.php): `GET /api/user-info`
* Requires `#[SessionUser]` — active frontend session (`sid` cookie)
* 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
* Requires `oxid-esales/session-authentication-component`
* **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)
* 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)
* Requires `oxid-esales/session-authentication-component`
* Each example follows the same layered structure: Controller → Service (interface) → DAO (interface) → DataObject
* [Service wiring](src/ApiEntrypoint/ProductInfo/services.yaml) — public controller, private service and DAO

**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 @@ -31,7 +31,9 @@
"codeception/module-webdriver": "^4.0",
"oxid-esales/codeception-modules": "dev-b-7.5.x",
"oxid-esales/codeception-page-objects": "dev-b-7.5.x",
"oxid-esales/developer-tools": "dev-b-7.5.x"
"oxid-esales/developer-tools": "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"
},
"conflict": {
"oxid-esales/oxideshop-ce": "<7.5"
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"
}
}
}
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/DataObject/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\DataObject;

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

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

public function getGreeting(): string
{
return $this->greeting;
}
}
34 changes: 34 additions & 0 deletions src/ApiEntrypoint/AdminInfo/Service/AdminInfoService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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\DataObject\AdminInfo;
use OxidEsales\ExamplesModule\Core\Module as ModuleCore;

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

public function getAdminInfo(string $username): AdminInfo
{
$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\DataObject\AdminInfo;

interface AdminInfoServiceInterface
{
public function getAdminInfo(string $username): AdminInfo;
}
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,42 @@
<?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();

return new JsonResponse([
'customerGroups' => array_map(
static fn($group) => [
'groupId' => $group->getGroupId(),
'title' => $group->getTitle(),
'count' => $group->getCount(),
],
$groups,
),
'total' => $this->customerGroupService->getTotalCustomerCount(),
]);
}
}
57 changes: 57 additions & 0 deletions src/ApiEntrypoint/CustomerGroup/Dao/CustomerGroupCountDao.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

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

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao;

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

readonly class CustomerGroupCountDao implements CustomerGroupCountDaoInterface
{
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();

return array_values(array_map(
static fn(array $row) => new CustomerGroupCount(
groupId: $row['groupId'],
title: $row['title'],
count: (int) $row['customerCount'],
),
$rows,
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

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

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao;

use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\DataObject\CustomerGroupCount;

interface CustomerGroupCountDaoInterface
{
/** @return list<CustomerGroupCount> */
public function getCustomerGroupCounts(): array;
}
35 changes: 35 additions & 0 deletions src/ApiEntrypoint/CustomerGroup/DataObject/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\DataObject;

readonly class CustomerGroupCount
{
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;
}
}
36 changes: 36 additions & 0 deletions src/ApiEntrypoint/CustomerGroup/Service/CustomerGroupService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

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

declare(strict_types=1);

namespace OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Service;

use OxidEsales\ExamplesModule\ApiEntrypoint\CustomerGroup\Dao\CustomerGroupCountDaoInterface;

readonly class CustomerGroupService implements CustomerGroupServiceInterface
{
public function __construct(
private CustomerGroupCountDaoInterface $groupCountDao,
) {
}

/** @inheritDoc */
public function getCustomerGroupCounts(): array
{
return $this->groupCountDao->getCustomerGroupCounts();
}

public function getTotalCustomerCount(): int
{
return array_sum(
array_map(
static fn($group) => $group->getCount(),
$this->getCustomerGroupCounts(),
),
);
}
}
Loading
Loading