PHPackages                             vasyaxy/crm-application-module - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. vasyaxy/crm-application-module

ActiveLibrary

vasyaxy/crm-application-module
==============================

CRM Application Module

3.1.0.1(2y ago)05MITPHPPHP ^8.1

Since Mar 21Pushed 2y agoCompare

[ Source](https://github.com/VasyaXY/crm-application-module)[ Packagist](https://packagist.org/packages/vasyaxy/crm-application-module)[ Docs](https://remp2030.com)[ RSS](/packages/vasyaxy-crm-application-module/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (1)Dependencies (33)Versions (3)Used By (0)

CRM Application Module
======================

[](#crm-application-module)

[![Translation status @ Weblate](https://camo.githubusercontent.com/fb69cfc23a526ef80b341e7cfd1509a5ba5b9eea700da61be7193e4ea6141e4f/68747470733a2f2f686f737465642e7765626c6174652e6f72672f776964676574732f72656d702d63726d2f2d2f6170706c69636174696f6e2d6d6f64756c652f7376672d62616467652e737667)](https://hosted.weblate.org/projects/remp-crm/application-module/)

Configuration
-------------

[](#configuration)

### Redis

[](#redis)

You can configure default Redis keys prefix, which is used if implementation using RedisClientTrait enables prefixing via `useRedisKeysPrefix()` method.

```
crm_application:
    redis_client_factory:
        prefix: foo_
```

You can turn on prefixing for specific service using `RedisClientTrait` by calling `useRedisKeysPrefix()` method in configuration.

```
configsCache:
		setup:
			- useRedisKeysPrefix()
```

#### Replication

[](#replication)

CRM supports Redis replication with use of [Redis Sentinel](https://redis.io/topics/sentinel). To enable the use of sentinel, add following to your `config.neon`:

```
crm_application:
	redis_client_factory:
		replication:
			service: my-sentinel-service
			sentinels:
				- [scheme: tcp, host: sentinel-host-a, port: 26379]
				- [scheme: tcp, host: sentinel-host-b, port: 26379]
				- [scheme: tcp, host: sentinel-host-c, port: 26379]
```

### Database

[](#database)

#### Replication

[](#replication-1)

CRM allows you to configure secondary database connections used for read-only queries to lower the load of the primary database server. Add these blocks to your CRM configuration:

```
# replica connection parameters
parameters:
	database:
		replica:
			adapter: @environmentConfig::get('CRM_DB_REPLICA_ADAPTER') # ENV variables are arbitrary, feel free to change them
			host: @environmentConfig::get('CRM_DB_REPLICA_HOST')
			name: @environmentConfig::get('CRM_DB_REPLICA_NAME')
			user: @environmentConfig::get('CRM_DB_REPLICA_USER')
			password: @environmentConfig::get('CRM_DB_REPLICA_PASS')
			port: @environmentConfig::get('CRM_DB_REPLICA_PORT')

# configure replicas so the CRM is aware of them; add as many instances as you need, each under its own key
database:
	replica:
		dsn: ::sprintf("%s:host=%s;dbname=%s;port=%s", %database.replica.adapter%, %database.replica.host%, %database.replica.name%, %database.replica.port%)
		user: %database.replica.user%
		password: %database.replica.password%
		options:
			lazy: yes

# configure repositories to use replicaConfig (otherwise all queries would go to the primary database)
decorator:
	Crm\ApplicationModule\Repository:
		setup:
			- setReplicaConfig(@replicaConfig) # this enables the support for reading from replicas

services:
	replicaConfig:
		setup:
			# configure application which of the known replica DBs can be used for reads
			- addReplica(@database.replica.context)

			# configure application which DB tables can be used for replica reads
			# by default, every query still goes to the primary DB server; you need to explicitly allow tables, which are safe to be queried from replica
			- addTable(configs)
			- addTable(config_categories)
			- addTable(countries)
			- addTable(api_tokens)
			- addTable(admin_access)
			- addTable(admin_groups_access)
```

Commands
--------

[](#commands)

### `application:heartbeat`

[](#applicationheartbeat)

If your Hermes worker (`application:hermes_worker`) is losing connection to MySQL after a long period of inactivity, add `application:heartbeat` into your scheduler *(e.g. crontab)* with small interval *(e.g. 1 minute)*.

> **WARNING: Change paths to PHP and command.php according to your installation.**

```
# emit heartbeat event
*/1 * * * * /usr/bin/php /var/www/html/bin/command.php application:heartbeat
```

Event is handled by `HeartbeatMysql` handler which pings MySQL. This simple process keeps Hermes worker alive.

### `application:hermes_shutdown`

[](#applicationhermes_shutdown)

Command `application:hermes_shutdown` can be used to gracefully shutdown Hermes worker and all other workers which integrate Hermes' `RestartInterface` *(eg. Scenarios worker present in [ScenariosModule](https://github.com/remp2020/crm-scenarios-module))*. This can be used after CRM update when it's needed to reload all workers to new version.

> **WARNING: Change paths to PHP and command.php according to your installation.**

```
/usr/bin/php /var/www/html/bin/command.php application:hermes_shutdown
```

User confirmation is required to proceed with shutdown of all worker.

In case you need to run this command without user interaction *(eg. CI, tests)*, use `--assume-yes` flag:

```
/usr/bin/php /var/www/html/bin/command.php application:hermes_shutdown --assume-yes
```

### `application:cleanup`

[](#applicationcleanup)

Command can be used to clean up data from repositories, which you don't need to keep forever (i.e. logs).

By default, the command deletes data older than 2 months (based on the `created_at` column). You can change the default threshold time before which the command deletes old repository data, and also column which it uses by using (in your project configuration file):

```
autoLoginTokensRepository:
	setup:
		- setRetentionThreshold('now', 'valid_at')
changePasswordsLogsRepository:
	setup:
		- setRetentionThreshold('-12 months')
userActionsLogRepository:
	setup:
		- setRetentionThreshold('-12 months')
```

Components
----------

[](#components)

#### [FrontendMenu](https://github.com/remp2020/crm-application-module/blob/d35256140dba71e7839955da7a5205b3241f1923/src/components/FrontendMenu/FrontendMenu.php)

[](#frontendmenu)

User-facing frontend menu expected to be used in your application layout.

Example useUse within your layout by using:

```
{control fronendMenu}
```

You can override the default layout of menu in your `config.local.neon`:

```
# ...
services:
    frontendMenu:
        setup:
            - setTemplate('../../../../../app/modules/DemoModule/templates/frontend_menu.latte')
# ...
```

Preview[![alt text](docs/frontend_menu.png "Frontend menu")](docs/frontend_menu.png)

##### FrontendMenuDataProviderInterface

[](#frontendmenudataproviderinterface)

Interface which dataproviders have to implement to be able to edit `FrontendMenu` items. Dataproviders have to be attached to `frontend_menu` dataproviders placeholder.

Example useIn this example `DemoFrontendMenuDataProvider` removes menu item from frontend menu by link.

```
use Crm\ApplicationModule\DataProvider\DataProviderManager;

class DemoModule
{
    public function registerDataProviders(DataProviderManager $dataProviderManager)
    {
        $dataProviderManager->registerDataProvider(
            'frontend_menu',
            $this->getInstance(DemoFrontendMenuDataProvider::class)
        );
    }
}
```

```
use Crm\ApplicationModule\DataProvider\DataProviderException;
use Crm\ApplicationModule\DataProvider\FrontendMenuDataProviderInterface;
use Crm\ApplicationModule\Menu\MenuContainerInterface;

class DemoFrontendMenuDataProvider implements FrontendMenuDataProviderInterface
{

    public function provide(array $params): void
    {
        if (!isset($params['menuContainer'])) {
            throw new DataProviderException('missing [menuContainer] within data provider params');
        }

        /** @var MenuContainerInterface $menuContainer */
        $menuContainer = $params['menuContainer'];
        $menuContainer->removeMenuItemByLink(':Invoices:Invoices:invoiceDetails');
    }
}
```

#### Graphs

[](#graphs)

Following is a set of various charts provided by the `ApplicationModule` out of the box.

##### [GoogleBarGraph](src/Components/Graphs/GoogleBarGraph/GoogleBarGraph.php)

[](#googlebargraph)

Simple bar graph based on Google charts. Usually used within GoogleBarGraphGroup component, but can be also used separately if you need to manage your groups (keys within `$data` parameter) manually.

Example useTo use the chart, create similar method in your presenter or widget:

```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
    }

    public function createComponentGoogleUserSubscribersRegistrationSourceStatsGraph()
    {
        $control = $this->factory->create();

        $results = $this->database->table('subscriptions')
            ->where('subscriptions.start_time < ?', $this->database::literal('NOW()'))
            ->where('subscriptions.end_time > ?', $this->database::literal('NOW()'))
            ->group('user.source')
            ->select('user.source, count(*) AS count')
            ->order('count DESC')
            ->fetchAll();

        $data = [];

        foreach ($results as $row) {
            $data[$row['source']] = $row['count'];
        }

        $control->addSerie($this->translator->translate('dashboard.users.active_sub_registrations.serie'), $data);

        return $control;
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

        {control googleUserSubscribersRegistrationSourceStatsGraph}

```

preview[![alt text](docs/bar_graph.png "Google bar graph")](docs/bar_graph.png)

##### [GoogleBarGraphGroup](src/Components/Graphs/GoogleBarGraphGroup/GoogleBarGraphGroup.php)

[](#googlebargraphgroup)

Simple bar graph based on Google charts. Component is able to create multiple groups based on `->setGroupBy` method and results of your query. Internally uses `GoogleBarGraph` to render the chart.

example```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
    }

    public function createComponentGoogleUserActiveSubscribersRegistrationsSourceStatsGraph(GoogleBarGraphGroupControlFactoryInterface $factory)
    {
        $graphDataItem = new GraphDataItem();

        $graphDataItem->setCriteria(
            (new Criteria)->setTableName('payments')
                ->setTimeField('created_at')
                ->setJoin('JOIN users ON payments.user_id = users.id')
                ->setWhere("AND payments.status = '" . PaymentsRepository::STATUS_PAID . "'")
                ->setGroupBy('users.source') // setSeries('users.source') // setValueField('count(*)')
                ->setStart(DateTime::from($this->dateFrom))
                ->setEnd(DateTime::from($this->dateTo))
        );

        $control = $factory->create();
        $control->setGraphTitle($this->translator->translate('dashboard.payments.registration.title'))
            ->setGraphHelp($this->translator->translate('dashboard.payments.registration.tooltip'))
            ->addGraphDataItem($graphDataItem);

        return $control;
    }
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

        {control googleUserActiveSubscribersRegistrationsSourceStatsGraph}

```

preview[![alt text](docs/bar_graph_group.png "Google bar graph group")](docs/bar_graph_group.png)

##### [GoogleLineGraph](src/Components/Graphs/GoogleLineGraph/GoogleLineGraph.php)

[](#googlelinegraph)

Simple line graph based on Google charts. This component is only being used by `GoogleLineGraphGroup` and is not advised to be used directly unless you really need to provide raw data for the chart.

preview[![alt text](docs/line_graph.png "Line graph")](docs/line_graph.png)

##### [GoogleLineGraphGroup](src/Components/Graphs/GoogleLineGraphGroup/GoogleLineGraphGroup.php)

[](#googlelinegraphgroup)

Simple line graph based on Google charts. Component is able to create multiple series based on data grouped within built query.

example```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
    }

    public function createComponentGoogleSubscriptionsEndGraph(GoogleLineGraphGroupControlFactoryInterface $factory)
    {
        $items = [];

        $graphDataItem = new 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;

        $graphDataItem = new GraphDataItem();
        $graphDataItem->setCriteria((new Criteria())
            ->setTableName('subscriptions')
            ->setWhere('AND next_subscription_id IS NOT NULL')
            ->setTimeField('end_time')
            ->setValueField('count(*)')
            ->setStart($this->dateFrom)
            ->setEnd($this->dateTo));
        $graphDataItem->setName($this->translator->translate('dashboard.subscriptions.ending.withnext.title'));
        $items[] = $graphDataItem;

        $control = $factory->create()
            ->setGraphTitle($this->translator->translate('dashboard.subscriptions.ending.title'))
            ->setGraphHelp($this->translator->translate('dashboard.subscriptions.ending.tooltip'));

        foreach ($items as $graphDataItem) {
            $control->addGraphDataItem($graphDataItem);
        }
        return $control;
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

        {control googleSubscriptionsEndGraph}

```

preview[![alt text](docs/line_graph_group.png "Line graph group")](docs/line_graph_group.png)

##### [InlineBarGraph](src/Components/Graphs/InlineBarGraph/InlineBarGraph.php)

[](#inlinebargraph)

Inline bar chart is meant to be used directly within grids or with other inline bar charts to provide quick information about the data.

exampleFollowing is example of multiple inline bar charts that are displayed in the grid listing of available payment gateways.

```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    /** @var \Crm\ApplicationModule\Graphs\GraphData\GraphData @inject */
    public $graphData;

    // ...
    public function renderDefault()
    {
    }

    public function createComponentSmallGraph()
    {
        return new Multiplier(function ($id) {
            $control = new InlineBarGraph;

            $graphDataItem = new GraphDataItem();
            $graphDataItem
                ->setCriteria(
                    (new Criteria())
                        ->setTableName('payments')
                        ->setWhere('AND payment_gateway_id = ' . $id)
                        ->setGroupBy('payment_gateway_id')
                        ->setStart('-3 months')
                );

            $this->graphData->clear();
            $this->graphData->addGraphDataItem($graphDataItem);
            $this->graphData->setScaleRange('day');

            $data = $this->graphData->getData();
            if (!empty($data)) {
                $data = array_pop($data);
            }

            $control->setGraphTitle($this->translator->translate('payments.admin.payment_gateways.small_graph.title'))
                ->addSerie($data);
            return $control;
        });
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

    {foreach $paymentGateways as $gateway}

        {control smallGraph-$gateway->id}

    {/foreach}

```

preview[![alt text](docs/inline_bar_graph.png "Inline bar graph")](docs/inline_bar_graph.png)

##### [SmallBarchart](src/Components/Graphs/SmallBarchart/SmallBarGraph.php)

[](#smallbarchart)

`SmallBarChart` is targeted to be used within widgets across the CRM admin, but can be also used in user-facing frontend to provide simple countable information.

example```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
    }

    public function createComponentPaidPaymentsSmallBarGraph(SmallBarGraphControlFactoryInterface $factory)
    {
        return $this->generateSmallBarGraphComponent(PaymentsRepository::STATUS_PAID, 'Paid', $factory);
    }

    private function generateSmallBarGraphComponent($status, $title, SmallBarGraphControlFactoryInterface $factory)
    {
        $data = $this->paymentsHistogramFactory->paymentsLastMonthDailyHistogram($status);

        $control = $factory->create();
        $control->setGraphTitle($title)->addSerie($data);

        return $control;
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

        {control paidPaymentsSmallBarGraph}

```

preview[![alt text](docs/small_bar_graph.png "Small bar graph")](docs/small_bar_graph.png)

##### [GoogleSankeyGraphGroup](src/Components/Graphs/GoogleSankeyGraphGroup/GoogleSankeyGraphGroup.php)

[](#googlesankeygraphgroup)

A sankey graph is a component based on Google Sankey diagram. It's used to depict a flow from one set of values to another.

example```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
    }

    public function createComponentGoogleSankeyGraph(GoogleSankeyGraphGroupControlFactoryInterface $factory)
    {
        $graph = $factory->create();
        $graph->setGraphHelp($this->translator->translate('Graph help');
        $graph->setGraphTitle($this->translator->translate('Graph title');
        $graph->setRows([
            ['A', 'X', 1],
            ['A', 'Y', 3],
            ['A', 'Z', 2],
            ['B', 'X', 4],
            ['B', 'Y', 2],
            ['B', 'Z', 2],
        ]);
        $graph->setColumnNames('From', 'To', 'Count');
        return $graph;
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed:

```

        {control googleSankeyGraph}

```

preview[![alt text](docs/sankey_graph.png "Line graph group")](docs/sankey_graph.png)

#### [VisualPaginator](src/Components/VisualPaginator/VisualPaginator.php)

[](#visualpaginator)

Paginator is used to limit and offset your results displayed in lists all around the system. Paginator keeps the current page/limit and provides the information to your data-fetching blocks of code.

exampleFollowing is a paginator usage for scenarios listing.

```
namespace Crm\DemoModule\Presenters;

class DemoPresenter extends \Crm\AdminModule\Presenters\AdminPresenter
{
    // ...
    public function renderDefault()
    {
        $scenarios = $this->scenariosRepository->all();

        $filteredCount = $this->template->filteredCount = $products->count('*');

        $vp = new VisualPaginator();
        $this->addComponent($vp, 'scenarios_vp');
        $paginator = $vp->getPaginator();
        $paginator->setItemCount($filteredCount);
        $paginator->setItemsPerPage(50);

        $this->template->vp = $vp;
        $this->template->scenarios = $scenarios->limit($paginator->getLength(), $paginator->getOffset());
    }
    // ...
}
```

In your `templates/Demo/default.latte` template, use the component as needed (usually below or above the listing). Name of the control should be the same as you used within `->addComponent` 2nd argument.

```
{control scenarios_vp}
```

preview[![alt text](docs/visual_paginator.png "Visual paginator")](docs/visual_paginator.png)

#### Widgets

[](#widgets)

Following is a set of widget wrappers provided by `ApplicationModule` to be used by your widgets. Application provides three set of wrappers:

##### [SimpleWidget](src/Components/Widgets/SimpleWidget/SimpleWidget.php)

[](#simplewidget)

Simple widget is the component allowing simple extension of modules's view by other modules. Module can provide placeholder for widgets in the action's template (in `.latte` file) and other module can register their implementations of widget in their Module class.

You can read more about creating and registering widgets in CRM skeleton documentation available at [github.com/remp2020/crm-skeleton](https://github.com/remp2020/crm-skeleton#registerWidgets).

##### [SingleStatWidget](src/Components/Widgets/SingleStatWidget/SingleStatWidget.php)

[](#singlestatwidget)

Widget provides wrapper for simple table with single statistic - each provided by separate widget implementation. The primary scenario for this use are dashboards.

preview[![alt text](docs/simple_widget.png "Simple widget")](docs/simple_widget.png)

Database tables migration
-------------------------

[](#database-tables-migration)

Because of need of changing primary keys (int -&gt; bigint), in tables that contain lots of data (or have risk of overflowing primary key if its int), we had to create migration process. Since some tables are very exposed and cannot be locked for more than a couple of seconds, we decided to create new tables, migrate the data manually and keep the old and new table in sync while migrating.

*This migration process is necessary only for installations after specific version for specific table, and is two steps process.*

### Audit logs migration (installed before 2.5.0)

[](#audit-logs-migration-installed-before-250)

Consists of `audit_logs` table migration.

Steps:

1. Run phinx migrations command `phinx:migrate`, which creates new table `audit_logs_v2` (in case there is no data in table, migration just changes type of primary key and next steps are not needed).
2. Run command `application:convert_audit_logs_to_bigint`, which copies data from old table to new (e.g. `audit_logs` to `audit_logs_v2`). Command will after successful migration atomically rename tables (e.g. `audit_logs` -&gt; `audit_logs_old` and `audit_logs_v2` -&gt; `audit_logs`), so when the migration ends only new tables are used.

It's recommended to run `application:bigint_migration_cleanup audit_logs` command, at least 2 weeks (to preserve backup data, if some issue emerges) after successful migration to drop left-over tables.

###  Health Score

24

—

LowBetter than 32% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity4

Limited adoption so far

Community16

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor2

2 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

779d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e30e7c21a3bb88b7fcbace41fff4f94457f1adfc990e102cbab0aed264691ccf?d=identicon)[VasyaXY](/maintainers/VasyaXY)

---

Top Contributors

[![rootpd](https://avatars.githubusercontent.com/u/812909?v=4)](https://github.com/rootpd "rootpd (222 commits)")[![markoph](https://avatars.githubusercontent.com/u/6843562?v=4)](https://github.com/markoph "markoph (161 commits)")[![miroc](https://avatars.githubusercontent.com/u/1230714?v=4)](https://github.com/miroc "miroc (101 commits)")[![zoldia](https://avatars.githubusercontent.com/u/1526070?v=4)](https://github.com/zoldia "zoldia (29 commits)")[![tomaj](https://avatars.githubusercontent.com/u/446736?v=4)](https://github.com/tomaj "tomaj (28 commits)")[![Matefko](https://avatars.githubusercontent.com/u/22897457?v=4)](https://github.com/Matefko "Matefko (17 commits)")[![lubos-michalik](https://avatars.githubusercontent.com/u/63700066?v=4)](https://github.com/lubos-michalik "lubos-michalik (8 commits)")[![weblate](https://avatars.githubusercontent.com/u/1607653?v=4)](https://github.com/weblate "weblate (5 commits)")[![mikoczy](https://avatars.githubusercontent.com/u/14105084?v=4)](https://github.com/mikoczy "mikoczy (3 commits)")[![nakashu](https://avatars.githubusercontent.com/u/1550659?v=4)](https://github.com/nakashu "nakashu (2 commits)")[![VasyaXY](https://avatars.githubusercontent.com/u/68634723?v=4)](https://github.com/VasyaXY "VasyaXY (2 commits)")[![mmacsodi-fcm](https://avatars.githubusercontent.com/u/169140294?v=4)](https://github.com/mmacsodi-fcm "mmacsodi-fcm (1 commits)")[![kipanshi](https://avatars.githubusercontent.com/u/413509?v=4)](https://github.com/kipanshi "kipanshi (1 commits)")[![lulco](https://avatars.githubusercontent.com/u/9377319?v=4)](https://github.com/lulco "lulco (1 commits)")[![Brezak](https://avatars.githubusercontent.com/u/59848927?v=4)](https://github.com/Brezak "Brezak (1 commits)")[![PayteR](https://avatars.githubusercontent.com/u/1248565?v=4)](https://github.com/PayteR "PayteR (1 commits)")[![twoleds](https://avatars.githubusercontent.com/u/614302?v=4)](https://github.com/twoleds "twoleds (1 commits)")

### Embed Badge

![Health badge](/badges/vasyaxy-crm-application-module/health.svg)

```
[![Health](https://phpackages.com/badges/vasyaxy-crm-application-module/health.svg)](https://phpackages.com/packages/vasyaxy-crm-application-module)
```

###  Alternatives

[nette/nette

👪 Nette Framework - innovative framework for fast and easy development of secured web applications in PHP (metapackage)

1.6k2.8M334](/packages/nette-nette)[concrete5/core

Concrete core subtree split

19159.3k48](/packages/concrete5-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
