PHPackages                             conceptte/testcrm - 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. [Framework](/categories/framework)
4. /
5. conceptte/testcrm

ActiveLibrary[Framework](/categories/framework)

conceptte/testcrm
=================

MiniCRM extension for Nette Framework (test example)

1.1.4(1mo ago)035↓50%MITPHPPHP &gt;=8.2

Since Jun 15Pushed 1mo agoCompare

[ Source](https://github.com/conceptte/testcrm)[ Packagist](https://packagist.org/packages/conceptte/testcrm)[ RSS](/packages/conceptte-testcrm/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (2)Versions (22)Used By (0)

MiniCRM - Test exersise
=======================

[](#minicrm---test-exersise)

Journey
-------

[](#journey)

During the development of this package, I focused on creating a clean and modular architecture that can be easily integrated into any Nette-based application. I wanted to demonstrate best practices in Nette development, such as using extensions, presenters, and repositories to keep the code organized and maintainable.

I went throwgh such opics as:

- Nette extensions
    - configuration
    - bootstrap and services registration
- Dependency injection
    - services registration and usage
    - constructor injection
    - Attributes for DI
- Routing for clean URLs
    - route definitions
- Presenters
    - parameters injection
    - persistence parameters
    - forms
    - reusable components
    - redraw and snippets
- Form handling
- Latte basics
    - templates and layouts
    - blocks and snippets
- Database interactions with repositories
    - Explorer
- API design for JSON endpoints
- Security practices (CSRF, SQL injection prevention, XSS protection)

About the app
-------------

[](#about-the-app)

This package is built as a Nette extension, not as code mixed into the host app. That gives a few clear benefits:

- Host project only registers extension and routes.
- logic is in one place, not spread in many app files.
- host app can replace services by interface without editing core package code.

Main gap of extension in current version:

- (`fixed by custom copying assets files`) Asset pipeline is limited: styles are kept in Latte layout blocks, not in fully separate built assets.
- Sorting and filtering is basic, no complex queries or UI for that.
- No rate limiting or other protections for API endpoints.
- Error handling is basic, just showing messages without logging or detailed responses.
- No tests included, so it's not covered for edge cases or regressions.

Installation ([Fast dockerized setup:](https://github.com/conceptte/testcrm-host.git))
--------------------------------------------------------------------------------------

[](#installation-fast-dockerized-setup)

I recomend to use the dockerized setup for quick testing. It includes a simple host app with the extension installed and configured, so you can see it in action right away. You can find it here:

1. Install

```
composer require conceptte/minicrm
```

2. Register in config:

```
extensions:
    minicrm: Mtr\MiniCRM\MiniCRMExtension
```

3. Add routes in `app/Core/RouterFactory.php`:

```
$router->add(\Mtr\MiniCRM\Routing\RouterFactory::create());
```

4. Add assets to `www/assets` (temporary solution, should be improved in future):

```
mkdir -p www/assets && cp -r vendor/conceptte/minicrm/assets/minicrm www/assets/minicrm
```

5. Create and seed database:

```
php vendor/conceptte/minicrm/database/schema.php
#basically it: mysql < vendor/conceptte/minicrm/database/schema.sql
php vendor/conceptte/minicrm/database/seed.php  # optional - adds test data
```

### Basic simple structure:

[](#basic-simple-structure)

```
├── assets/                   # CSS and JS files
├── config/                   # Extension configuration
├── database/                 # SQL schema and seed data
├── src/
    ├── API/V1/               # JSON endpoints
        ├── Presentation/     # API presenters
        ├── Request/          # Request DTOs
        ├── Resource/         # API resources
    ├── Exception/            # Custom exceptions
    ├── Routing/              # Where routes are defined
    ├── Presentation/         # Pages and forms
    ├── Repository/           # Gets data from database
    ├── MiniCRMExtension.php  # Main extension class
    ├── ConfigNodes.php       # Enum for config keys

```

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

[](#configuration)

Extension configuration is done in `config/minicrm.php`

I decided to use PHP for configuration instead of NEON because it allows for more flexibility And Nette supports it : `src/MiniCRMExtension.php`:

```
public function loadConfiguration(): void
{
    $this->extConfig = $this->loadFromFile(__DIR__ . '/../config/minicrm.php') ?? [];
}
```

You can easily swap out implementations or use real classes without needing a separate DI container configuration.

```
$mapiv1 = 'minicrm.api.v1';

return [
    'minicrm' => [
        'mapping' => [
            'MiniCRM' => 'Mtr\MiniCRM\Presentation\*\*Presenter',
            'MiniCRMAPIV1' => 'Mtr\MiniCRM\API\V1\Presentation\*\*Presenter',
        ],
        'services' => [
            'paginator' => Paginator::class,
            'paginationControl' => PaginationControl::class,
            'customersRepository' =>[
                'type' => CustomersRepositoryInterface::class,
                'create' => CustomersRepository::class,
            ],
            'activityRepository' => [
                'type' => ActivityRepositoryInterface::class,
                'create' => ActivityRepository::class,
            ],
            'commentsRepository' => [
                'type' => CommentsRepositoryInterface::class,
                'create' => CommentsRepository::class,
            ],

            "{$mapiv1}.request.customers.requestFactory" => CustomersRequestFactory::class,
            "{$mapiv1}.resource.customers.resourceFactory" => CustomerResourceFactory::class,
            "{$mapiv1}.resource.customers.collectionResourceFactory" => CustomerCollectionResourceFactory::class,
            "{$mapiv1}.resource.paginator.resourceFactory" => PaginatorResourceFactory::class,
        ],
    ],
];
```

### Routes:

[](#routes)

Router is defined in `src/Routing/RouterFactory.php`. It makkes group of routes for the app and API.

```
public static function create(): RouteList
    {
        $group = new RouteList();

        $minicrm = (new RouteList('MiniCRM'))->add(CustomersRouteFactory::create());
        $api = (new RouteList('MiniCRMAPI'))->add(ApiRouteFactory::create());

        $group
            ->add($minicrm)
            ->add($api);

        return $group;
    }
```

### Group of Frontend routes:

[](#group-of-frontend-routes)

- `/minicrm/customers` - Customer list page (search, filter, sort)
- `/minicrm/customers/{public_id}` - Customer details page (activities and history)
- `/minicrm/customers/{public_id}/activity/{id}` - Activity page (view details and comments)

### API routes group:

[](#api-routes-group)

- `/minicrm/api/v1/ping` - API to check if the service is alive
- `/minicrm/api/v1/customers` - API to get customer data as JSON
- `/minicrm/api/v1/customers/{public_id}` - API to get single customer details as JSON

Database tables
---------------

[](#database-tables)

[schema.sql](database/schema.sql) defines tables:

- `customers` - stores customer info
- `customer_activities` - stores customer activities
- `activity_comments` - stores comments on activities

Foreign keys and indexes are set for performance and data integrity.

Presenters
----------

[](#presenters)

I investigated and used native Nette functionality like:

- forms for comment add
- snippets for small page updates
- pagination for long lists
- AJAX with Naja for no full page reload

Presenters use repository services, pagination control as dependencies injected by Nette DI container.

Repository layer
----------------

[](#repository-layer)

They provide methods to get data from the database and are used by presenters and API endpoints. Registered as services in extension config, so they can be injected and swapped if needed.

API
---

[](#api)

Get customers as JSON:

```
GET /minicrm/api/v1/customers?q=&status=&page=&limit=

```

Response includes customer list and meta info:

```
{
  "success": true,
  "request": {
    "query": "wil",
    "status": "inactive",
    "page": 1,
    "limit": 2
  },
  "pagination": {
    "total": 3,
    "current": 1,
    "per_page": 2,
    "total_pages": 2
  },
  "data": [
    {
      "id": "c6a3170ff6d609",
      "name": "Willie Cummerata",
      "email": "bennie.dooley@example.net",
      "is_active": false,
      "total": {
        "activities": 28,
        "comments": 393
      },
      "meta": {
        "uri": "/minicrm/api/v1/customers/c6a3170ff6d609"
      }
    },
    {
      "id": "c6a317135e4bce",
      "name": "Prof. Bell Wilkinson",
      "email": "ruth.kuphal@example.net",
      "is_active": false,
      "total": {
        "activities": 29,
        "comments": 510
      },
      "meta": {
        "uri": "/minicrm/api/v1/customers/c6a317135e4bce"
      }
    }
  ]
}
```

Get single customer details:

```
GET /minicrm/api/v1/customers/{public_id}

```

Response includes customer info and some metadata:

```
{
  "success": true,
  "data": {
    "id": "c6a3170ff6d609",
    "name": "Willie Cummerata",
    "email": "bennie.dooley@example.net",
    "is_active": false,
    "total": {
      "activities": 28,
      "comments": 393
    },
    "meta": []
  }
}
```

Safety
------

[](#safety)

Used Nette's built-in features to protect against common web vulnerabilities:

- CSRF attacks - forms have protection tokens
- SQL injection - database uses safe queries
- XSS - text is automatically escaped
- Rate limiting - N/A (not implemented yet, but can be added)

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance91

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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

Every ~0 days

Total

21

Last Release

43d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/39bcfd110a5cc1f2d7ff687193670d00133cf415ad2b9959afaff24ff1e564fd?d=identicon)[vgalitsky](/maintainers/vgalitsky)

---

Top Contributors

[![vgalitsky](https://avatars.githubusercontent.com/u/1241206?v=4)](https://github.com/vgalitsky "vgalitsky (79 commits)")

### Embed Badge

![Health badge](/badges/conceptte-testcrm/health.svg)

```
[![Health](https://phpackages.com/badges/conceptte-testcrm/health.svg)](https://phpackages.com/packages/conceptte-testcrm)
```

###  Alternatives

[nette/bootstrap

🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.

68637.6M666](/packages/nette-bootstrap)[nette/nette

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

1.6k2.8M338](/packages/nette-nette)[nette/web-project

Nette: Standard Web Project

10993.3k](/packages/nette-web-project)[kdyby/events

Events for Nette Framework

582.1M57](/packages/kdyby-events)[contributte/webpack

Webpack integration for Nette Framework.

501.0M3](/packages/contributte-webpack)[contributte/middlewares

Middleware / Relay / PSR-7 support to Nette Framework

222.3M3](/packages/contributte-middlewares)

PHPackages © 2026

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