1. Go to this page and download the library: Download remp/crm-skeleton library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
remp / crm-skeleton example snippets
namespace Crm\DemoModule;
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// register your extension points based on the documentation below
}
class DemoPresenter extends \Crm\ApplicationModule\Presenters\FrontendPresenter
{
public function startup()
{
// in this example we want DemoPresenter be available only to logged in users
$this->onlyLoggedIn();
parent::startup();
}
public function renderDefault()
{
$this->template->userId = $this->getUser()->getUserId();
}
}
class DemoAdminPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
public function renderDefault()
{
// in Admin presenters even empty render methods are necessary
}
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerAdminMenuItems(\Crm\ApplicationModule\Menu\MenuContainerInterface $menuContainer)
{
$mainMenu = new \Crm\ApplicationModule\Menu\MenuItem('', '#', 'fa fa-link', 800);
$menuItem = new \Crm\ApplicationModule\Menu\MenuItem($this->translator->translate('api.menu.api_tokens'), ':Api:ApiTokensAdmin:', 'fa fa-unlink', 200);
$mainMenu->addChild($menuItem);
$menuContainer->attachMenuItem($mainMenu);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerFrontendMenuItems(\Crm\ApplicationModule\Menu\MenuContainerInterface $menuContainer)
{
$menuItem = new \Crm\ApplicationModule\Menu\MenuItem($this->translator->translate('payments.menu.payments'), ':Payments:Payments:My', '', 100);
$menuContainer->attachMenuItem($menuItem);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerEventHandlers(\League\Event\Emitter $emitter)
{
$emitter->addListener(
\Crm\UsersModule\Events\UserMetaEvent::class,
$this->getInstance(\Crm\ApplicationModule\Events\RefreshUserDataTokenHandler::class)
);
}
// ...
}
class DemoUserEvent extends \League\Event\AbstractEvent
{
private $userId;
public function __construct(int $userId, $value)
{
$this->userId = $userId;
}
public function getUserId(): int
{
return $this->userId;
}
}
class DemoRepository extends \Crm\ApplicationModule\Repository
{
private $emitter;
protected $tableName = 'demo';
public function __construct(
\Nette\Database\Context $database,
\League\Event\Emitter $emitter,
\Nette\Caching\IStorage $cacheStorage = null
) {
parent::__construct($database, $cacheStorage);
$this->emitter = $emitter;
}
public function save(\Nette\Database\Table\ActiveRow $user, string $demoValue)
{
$result = $this->insert([
'value' => $demoValue,
]);
if ($result) {
// HERE'S THE EXAMPLE OF EMITTING YOUR EVENT
$this->emitter->emit(new DemoUserEvent($user->id, $demoValue));
}
return $result;
}
}
class DemoHandler extends \League\Event\AbstractListener
{
public function handle(\League\Event\EventInterface $event)
{
$userId = $event->getUserId();
// do whathever handling you want to do
}
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerLazyWidgets(\Crm\ApplicationModule\Widget\LazyWidgetManagerInterface $widgetManager)
{
$widgetManager->registerWidget(
'admin.users.header',
\Crm\SubscriptionsModule\Components\MonthSubscriptionsSmallBarGraphWidget::class
);
}
// ...
}
class DemoWidget extends \Crm\ApplicationModule\Widget\BaseLazyWidget
{
private $templateName = 'demo.latte';
public function identifier()
{
return 'demowidget';
}
public function render()
{
$this->template->setFile(__DIR__ . DIRECTORY_SEPARATOR . $this->templateName);
$this->template->render();
}
}
class CacheCommand extends \Symfony\Component\Console\Command\Command
{
// ...
protected function configure()
{
$this->setName('application:cache')
->setDescription('Resets application cache (per-module)')
->addOption(
'tags',
null,
\Symfony\Component\Console\Input\InputOption::VALUE_OPTIONAL | \Symfony\Component\Console\Input\InputOption::VALUE_IS_ARRAY,
'Tag specifies which group of cache values should be reset.'
);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerHermesHandlers(\Tomaj\Hermes\Dispatcher $dispatcher)
{
$dispatcher->registerHandler(
'dummy-event',
$this->getInstance(\Crm\DemoModule\Hermes\FooHandler::class)
);
}
// ...
}
class DemoPresenter extends \Crm\ApplicationModule\Presenters\FrontendPresenter
{
/** @var Tomaj\Hermes\Emitter @inject */
public $hermesEmitter;
public function renderDefault($id)
{
$this->emitter->emit(new \Crm\ApplicationModule\Hermes\HermesMessage('dummy-event', [
'fooId' => $id,
'userId' => $this->getUser()->getId(),
]));
}
}
class FooHandler implements \Tomaj\Hermes\Handler\HandlerInterface
{
public function handle(\Tomaj\Hermes\MessageInterface $message): bool
{
$payload = $message->getPayload();
// your processing code
// return true if the execution went correctly, false other
return true;
}
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerAuthenticators(\Crm\ApplicationModule\Authenticator\AuthenticatorManagerInterface $authenticatorManager)
{
$authenticatorManager->registerAuthenticator(
$this->getInstance(\Crm\DemoModule\Authenticator\FooAuthenticator::class)
);
}
// ...
}
class FooAuthenticator extends \Crm\ApplicationModule\Authenticator\BaseAuthenticator
{
// ...
private $token;
public function setCredentials(array $credentials) : \Crm\ApplicationModule\Authenticator\AuthenticatorInterface
{
parent::setCredentials($credentials);
$this->token = $credentials['fooQuery'] ?? null;
return $this;
}
public function authenticate()
{
if ($this->token === null) {
return false;
}
$email = $this->tokenChecker->getEmailFromToken($this->token);
if (!$email) {
throw new \Nette\Security\AuthenticationException('invalid token', , \Crm\UsersModule\Auth\UserAuthenticator::IDENTITY_NOT_FOUND);
}
$user = $this->userManager->loadUserByEmail($email);
if (!$user) {
throw new \Nette\Security\AuthenticationException('invalid token', , \Crm\UsersModule\Auth\UserAuthenticator::IDENTITY_NOT_FOUND);
}
$this->addAttempt($user->email, $user, $this->source, \Crm\UsersModule\Repository\LoginAttemptsRepository::STATUS_TOKEN_OK);
return $user;
}
// ...
}
class UserMetaUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function data($userId)
{
$result = [];
foreach ($this->userMetaRepository->userMetaRows($userId)->where(['is_public' => true]) as $row) {
$result[] = [$row->key => $row->value];
}
return $result;
}
// ...
}
class UserMetaUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function download($userId)
{
$result = [];
foreach ($this->userMetaRepository->userMetaRows($userId) as $row) {
$result[] = [$row->key => $row->value];
}
return $result;
}
// ...
}
class PaymentsUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function downloadAttachments($userId)
{
$payments = $this->paymentsRepository->userPayments($userId)->where('invoice_id IS NOT NULL');
$files = [];
foreach ($payments as $payment) {
$invoiceFile = tempnam(sys_get_temp_dir(), 'invoice');
$this->invoiceGenerator->renderInvoicePDFToFile($invoiceFile, $payment->user, $payment);
$fileName = $payment->invoice->invoice_number->number . '.pdf';
$files[$fileName] = $invoiceFile;
}
return $files;
}
// ...
}
class OrdersUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function protect($userId): array
{
$exclude = [];
foreach ($this->ordersRepository->getByUser($userId)->fetchAll() as $order) {
$exclude[] = $order->shipping_address_id;
$exclude[] = $order->licence_address_id;
$exclude[] = $order->billing_address_id;
}
return [\Crm\UsersModule\User\AddressesUserDataProvider::identifier() => array_unique(array_filter($exclude), SORT_NUMERIC)];
}
// ...
}
class SubscriptionsUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function canBeDeleted($userId): array
{
$threeMonthsAgo = DateTime::from(strtotime('-3 months'));
if ($this->subscriptionsRepository->hasSubscriptionEndAfter($userId, $threeMonthsAgo)) {
return [false, $this->translator->translate('subscriptions.data_provider.delete.three_months_active')];
}
return [true, null];
}
// ...
}
class AddressChangeRequestsUserDataProvider implements \Crm\ApplicationModule\User\UserDataProviderInterface
{
// ...
public function delete($userId, $protectedData = [])
{
$this->addressChangeRequestsRepository->deleteAll($userId);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerSegmentCriteria(\Crm\ApplicationModule\Criteria\CriteriaStorage $criteriaStorage)
{
$criteriaStorage->register(
'users',
'foo_criteria',
$this->getInstance(\Crm\DemoModule\Segment\FooCriteria::class)
);
}
// ...
}
class ActiveSubscriptionCriteria implements \Crm\ApplicationModule\Criteria\CriteriaInterface
{
// ...
public function params(): array
{
return [
new \Crm\SegmentModule\Params\DateTimeParam(
"active_at",
"Active at",
"Filter only subscriptions active within selected period",
false
),
new \Crm\SegmentModule\Params\StringArrayParam(
"contains",
"Content types",
"Users who have access to selected content types",
false,
null,
null,
$this->contentAccessRepository->all()->fetchPairs(null, 'name')
),
new \Crm\SegmentModule\Params\StringArrayParam(
"type",
"Types of subscription",
"Users who have access to selected types of subscription",
false,
null,
null,
array_keys($this->subscriptionsRepository->availableTypes())
),
new \Crm\SegmentModule\Params\NumberArrayParam(
"subscription_type",
"Subscription types",
"Users who have access to selected subscription types",
false,
null,
null,
$this->subscriptionTypesRepository->all()->fetchPairs("id", "name")
),
new \Crm\SegmentModule\Params\BooleanParam(
"is_recurrent",
"Recurrent subscriptions",
"Users who had at least one recurrent subscription"
),
];
}
// ...
}
class ActiveSubscriptionCriteria implements \Crm\ApplicationModule\Criteria\CriteriaInterface
{
// ...
public function join(\Crm\SegmentModule\Params\ParamsBag $params): string
{
$where = [];
if ($params->has('active_at')) {
$where = array_merge($where, $params->datetime('active_at')->escapedConditions('subscriptions.start_time', 'subscriptions.end_time'));
}
if ($params->has('contains')) {
$values = $params->stringArray('contains')->escapedString();
$where[] = " content_access.name IN ({$values}) ";
}
if ($params->has('type')) {
$values = $params->stringArray('type')->escapedString();
$where[] = " subscriptions.type IN ({$values}) ";
}
if ($params->has('subscription_type')) {
$values = $params->numberArray('subscription_type')->escapedString();
$where[] = " subscription_types.id IN ({$values}) ";
}
if ($params->has('is_recurrent')) {
$where[] = " subscriptions.is_recurrent = {$params->boolean('is_recurrent')->number()} ";
}
return "SELECT DISTINCT(subscriptions.user_id) AS id, " . \Crm\SegmentModule\Criteria\Fields::formatSql($this->fields()) . "
FROM subscriptions
INNER JOIN subscription_types ON subscription_types.id = subscriptions.subscription_type_id
INNER JOIN subscription_type_content_access ON subscription_type_content_access.subscription_type_id = subscription_types.id
INNER JOIN content_access ON content_access.id = subscription_type_content_access.content_access_id
WHERE " . implode(" AND ", $where);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerRoutes(\Nette\Application\Routers\RouteList $router)
{
$router->prepend('/sign/in/', 'Users:Sign:in'); // use "prepend" to have your route resolved early and possibly override our routes
$router->addRoute('/promo', 'Foo:Promo:default'); // use "addRoute" to to have your route resolve regularly
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function cache(\Symfony\Component\Console\Output\OutputInterface $output, array $tags = [])
{
$output->writeln("<info>Refreshing user stats cache</info>");
$repository = $container->getByType(\Crm\UsersModule\Repository\UsersRepository::class);
$repository->totalCount(true, true);
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerLayouts(\Crm\ApplicationModule\LayoutManager $layoutManager)
{
$layoutManager->registerLayout('frontend', realpath(__DIR__ . '/templates/@frontend_layout.latte'));
}
// ...
}
class DemoModule extends \Crm\ApplicationModule\CrmModule
{
// ...
public function registerDataProviders(\Crm\ApplicationModule\DataProvider\DataProviderManager $dataProviderManager)
{
$dataProviderManager->registerDataProvider(
'subscriptions.dataprovider.ending_subscriptions',
$this->getInstance(DemoDataProvider::class)
);
}
// ...
}
public function createComponentGoogleSubscriptionsEndGraph(\Crm\ApplicationModule\Components\Graphs\GoogleLineGraphGroupControlFactoryInterface $factory)
{
$items = [];
// THE DEFAULT CHART ITEM PROVIDED BY GENERIC MODULE
$graphDataItem = new \Crm\ApplicationModule\Graphs\GraphDataItem();
$graphDataItem->setCriteria((new Criteria())
->setTableName('subscriptions')
->setTimeField('end_time')
->setValueField('count(*)')
->setStart($this->dateFrom)
->setEnd($this->dateTo));
$graphDataItem->setName($this->translator->translate('dashboard.subscriptions.ending.now.title'));
$items[] = $graphDataItem;
// CHART ITEMS PROVIDED BY OTHER MODULES
$providers = $this->dataProviderManager->getProviders('subscriptions.dataprovider.ending_subscriptions');
foreach ($providers as $sorting => $provider) {
$items[] = $provider->provide(['dateFrom' => $this->dateFrom, 'dateTo' => $this->dateTo]);
}
// ...
}
class DemoDataProvider
{
public function provide(array $params): \Crm\ApplicationModule\Graphs\GraphDataItem
{
if (!isset($params['dateFrom'])) {
throw new \Crm\ApplicationModule\DataProvider\DataProviderException('dateFrom param missing');
}
if (!isset($params['dateTo'])) {
throw new \Crm\ApplicationModule\DataProvider\DataProviderException('dateTo param missing');
}
$graphDataItem = new \Crm\ApplicationModule\Graphs\GraphDataItem();
$graphDataItem->setCriteria((new \Crm\ApplicationModule\Graphs\Criteria())
->setTableName('subscriptions')
->setJoin('LEFT JOIN payments ON payments.subscription_id=subscriptions.id
LEFT JOIN recurrent_payments ON payments.id = recurrent_payments.parent_payment_id')
->setWhere(' AND next_subscription_id IS NULL AND (recurrent_payments.id IS NULL OR recurrent_payments.state != \'active\' or recurrent_payments.status is not null or recurrent_payments.retries = 0)')
->setTimeField('end_time')
->setValueField('count(*)')
->setStart($params['dateFrom'])
->setEnd($params['dateTo']));
$graphDataItem->setName($this->translator->translate('dashboard.subscriptions.ending.nonext.title'));
return $graphDataItem;
}
}
bash
# refreshing ACL will create new ACL rule matching new admin actions
php bin/command.php user:generate_access
# seeding will assign access right to the newly generated action to superadmin role
php bin/command.php application:seed
bash
php bin/command.php application:cache
bash
php bin/command.php api:generate_access
php bin/command.php application:cleanup
php bin/command.php application:hermes_worker
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.