Core/Services/Master — lisans doğrulama, proje/ayar/uygulama yönetimi (7 dosya)
Doğrulanan kod tabanı:
c23b431f· Tarih: 2026-10-05 · Yayın: 0.9.7 = bu commit + sonrası; belge yalnız doğrulama anındaki kodu anlatır
Kaynak klasör:Core/Services/Master/— 7*.php= 5 kök +Data/2.
Envanter: 7 dosyanın 7'si anlatıldı.
Doğrulama platformu: Windows + PHP 8.3;LicenceAccessRule4 kayıtla,
MasterLicencesService::verify()sahte repository ile gerçekten çalıştırıldı.
1. Ne işe yarar, kim kullanır
Master veritabanına (projects, licences, applications, settings)
konuşan tek katman. Dört servis + bir saf kural sınıfı + iki sabit dosyası:
| Sınıf | Sorumluluk |
|---|---|
MasterProjectsService |
Proje oluşturma/güncelleme, sürüm sayacı, keşif önbelleği temizliği |
MasterLicencesService |
Lisans anahtarı üretme/doğrulama/iptal |
MasterApplicationsService |
Uygulama kaydı, sürüm kapısı |
MasterSettingsService |
Master ayar CRUD + önbellek yenileme |
LicenceAccessRule |
Kural SSoT — "geçerli lisans mı?" sorusunun tek cevabı |
Kimler çağırır: DatabaseGuardHandler (isUnrestricted() →
MasterLicencesService::forSubject()), VersionHandlers (version:next →
nextVersion()), yönetim paneli (createProject/updateProject).
Anayasa §8 uyumu: Hiçbiri doğrudan Database kullanmaz; hepsi
repository üzerinden yazar (masterProjectsRepository, masterLicenceRepository,
masterApplicationRepository).
2. Klasör/dosya envanteri (7/7)
| Dosya | Görev | Önemli public yöntemler |
|---|---|---|
LicenceAccessRule.php (184) |
Saf kural sınıfı (final). Durum tutmaz, DB'ye dokunmaz, new gerektirmez. |
static isUnrestricted(?array $record): bool, static isAnomalous(?array $record): bool, static isStatusBlocking(mixed $status): bool, static blockingReason(mixed $status): ?string, static isRecordUsable(?array $record): bool, static isKnownStatus(mixed): bool, static isKnownTier(mixed): bool, static isExpired(mixed $expiresAt): bool · korumalı: hasValue(), text(), statuses(), tiers() |
MasterProjectsService.php (277) |
Proje yaşam döngüsü + sürüm sayacı. protected $targetModel = 'master.projects'. |
createProject(array $data): array, updateProject(int $id,array $data): array, getProjectsByGroup(string $groupName): array, nextVersion(string $projectKey,bool $apply=false): array · korumalı: generateUniqueLicenseKey(?string $group=null,string $tier='FREE'), formatCustomPath(?string $path,string $projectKey), clearDiscoveryCache(?string $projectKey,?string $domain,?string $group=null) |
MasterLicencesService.php (248) |
Lisans karar katmanı (fail-closed). const MAX_KEY_ATTEMPTS = 5. |
setLicenceRepository(object), verify(string $key,?string $deviceHash=null): array, forSubject(string $type,int $id): ?array, issue(string $type,int $id,string $tier,?string $expiresAt): array, revoke(string $key): bool · korumalı: checkDevice(), generateKey(), result(), warn(), static subjectTypes(), static tiers() |
MasterApplicationsService.php (248) |
Uygulama kaydı + sürüm kapısı. | setApplicationRepository(object), getByAppKey(string $key): ?array, list(): array, versionGate(?string $version): ?string, register(array $data): array, setCurrentVersion(string $appKey,string $version,?string $url,?string $sha256): bool · korumalı: nullable(), warn() |
MasterSettingsService.php (142) |
Master ayar CRUD. | master(string $key,$default=null): ?string, category(string $category): array, refresh(string $key): void, createSetting(array $data): array, updateSetting(int $id,array $data): array |
Data/MasterLicenceConfig.php (58) |
Lisans alan/enum sabitleri. | STATUS_* (4), TIER_* (3), SUBJECT_* (2), KEY_PREFIX='RBN', DEFAULT_MAX_ACTIVATIONS=1, REASON_* (7) |
Data/MasterSettingsConfig.php (35) |
Master ayar sözlükleri. | const TYPES (5), const CATEGORIES (6) |
3. Akış — createProject() (MasterProjectsService.php:26-72)
createProject(['project_name'=>…, 'domain'=>…, …])
1. project_key = helper('text')->turkishSlug($data['project_name']) :29-30
2. isKeyUnique() false → {success:false, errors:['Proje anahtarı zaten kullanımda.']} :33-38
3. license_key = generateUniqueLicenseKey($group) (tier: FREE) :41-42
4. custom_path = formatCustomPath($data['custom_path'] ?? null, $key) :45
5. repository->createProjectIdentity([... 8 alan ...]) :49-61
· version = $data['version'] ?: Version::initial() :59
· 'license_key' ve 'status' KORUMALI alan → yetkili yol :47-48
6. başarılıysa clearDiscoveryCache(key, domain, group) :63-66
→ {success, errors}
updateProject() (:77-129) aynı akışı izler tek farkla: license_key
korunur, yoksa üretilir (:99-105) — yani güncelleme anahtarı üretmez.
Tuzak: isKeyUnique($key) güncellemede kendi $id'si hariç kontrol
eder (:92 isKeyUnique($key, $id)) — yani aynı anahtarla güncelleme
engellenmez.
4. Akış — nextVersion() (varsayılan KURU KOŞU) (:198-251)
Dönüş sözleşmesi (:196):
array{success:bool, project_key:string, current:string, next:string, written:bool, errors:string[]}
nextVersion('<proje>', $apply = false)
├─ boş anahtar → errors[] + return :206-209
├─ repo->findByProjectKey($key) null → errors[] + return :213-217
├─ current = Version::isValid($mevcut) ? $mevcut : Version::initial() :219-221
│ ← GEÇERSİZ kayıt güvenli başlangıca düşer, sayaç ORADAN devam eder
├─ next = Version::next(current) :222
├─ !$apply → {success:true, written:false} ve DÖN :224-227 ← varsayılan
├─ repo->updateProjectVersion($id, $next) :230
│ InvalidArgumentException → errors[] + return :231-234
└─ yazıldıysa clearDiscoveryCache(key, domain, group) :243-247
Tuzak (:242 yorumu): Önbellek temizlenmezse eski sürüm okunmaya devam eder.
clearDiscoveryCache() (:256-276) üç ayrı anahtar siler:
project_<key>, domain_<alan adı>, group_<grup>. Alan adı anahtarında
.test/.local soyulur ve port (:) kesilir (:268) — yerel ortamda
.test olmadan anahtar üretmek için.
5. Akış — MasterLicencesService::verify() (fail-closed) (:41-86)
verify($key, $deviceHash = null)
1. boş anahtar → REASON_UNKNOWN_KEY :46-48
2. repo->findByKey() → istisna → REASON_STORAGE_ERROR (uyarı + geçersiz) :50-55
3. satır yok/boş → REASON_UNKNOWN_KEY :57-59
4. !LicenceAccessRule::isRecordUsable($row) → REASON_INVALID_RECORD :66-68
5. LicenceAccessRule::blockingReason($status) → revoked|suspended|expired :72-74
6. LicenceAccessRule::isExpired($expires_at) → REASON_EXPIRED :77-79
7. deviceHash verilmiş ve checkDevice() false → REASON_DEVICE_MISMATCH :81-83
→ {valid:true, status, tier, expires_at, reason:null}
Dönüş şekli (:39):
array{valid: bool, status: ?string, tier: ?string, expires_at: ?string, reason: ?string}
5.1 Ölçülen çıktı (sahte repository ile gerçek verify() çağrısı)
verify('RBN-TEST-KEY')
→ {"valid":true, "status":"active", "tier":"PRO", "expires_at":null, "reason":null}
verify('RBN-TEST-KEY', 'cihash-besik')
→ {"valid":false, "status":"active", "tier":"PRO", "expires_at":null, "reason":"device_mismatch"}
5.2 Ölçülen LicenceAccessRule tablosu (4 kayıt)
| Kayıt | isStatusBlocking |
isKnownStatus |
isRecordUsable |
|---|---|---|---|
null |
false |
false |
false |
{status:'active', tier:'FREE'} |
false |
true |
true |
{status:'revoked', tier:'PRO'} |
true |
true |
true |
{status:'bilinmeyen', tier:'X'} |
false |
false |
false |
Kritik ayrım: isRecordUsable() yalnız tanınabilirlik kontrol eder —
revoked bir kayıt kullanılabilir tanınıyor (tanınabilir), ama
isStatusBlocking() engel bildiriyor ve verify() :72-74'te geçersiz döner.
Yani iki metot farklı sorulara cevap verir; isRecordUsable() tek başına
"lisans geçerli" demek değildir.
5.3 Kural metni (sınıf docblock :17-24)
Kısıtsız = tier FREE VE status active VE
(expires_atNULL/boş → SÜRESİZ)
(expires_atDOLU VEstrtotimeile ÇÖZÜLEMEYEN → KISITLI = fail-closed)
(expires_atDOLU VE çözülüp geçmiş → KISITLI)
Neden ayrı sınıf (docblock :10-15, B-05): "geçerli lisans" sorusu iki
katmanda soruluyordu ve iki yerde farklı yazılmıştı
(DatabaseGuardHandler::isUnrestricted() ve MasterLicencesService::verify()).
Kural bir kez yazılır; kapı ve servis aynı cevabı alır.
Neden new yok (docblock :26-32): Saf kural sınıfı — durum tutmaz, DB'ye
dokunmaz, yalnız statik metot. Bu yüzden keşif/IoC kaydı gerekmez (Anayasa §1
new yasağı ve §5 "yalnız bileşen kaydı").
6. Akış — MasterApplicationsService::versionGate()
versionGate(?string $version): ?string (:108) sürüm eşiğini denetler:
uygulamanın min_version değeri istemci sürümünden yüksekse reddedilen
sürümü döner. register() (:131) yeni uygulama kaydı üretir;
setCurrentVersion() (:192) url + sha256 ile birlikte günceller —
yani bütünlük doğrulaması veri olarak saklanır.
7. Yapılandırma ve sabitler (ölçülen)
MasterLicenceConfig (tümü ölçüldü):
| Sabit | Değer |
|---|---|
STATUS_ACTIVE / SUSPENDED / REVOKED / EXPIRED |
active / suspended / revoked / expired |
TIER_FREE / LIFETIME / PRO |
FREE / LIFETIME / PRO |
SUBJECT_PROJECT / SUBJECT_APPLICATION |
project / application |
KEY_PREFIX |
RBN |
DEFAULT_MAX_ACTIVATIONS |
1 |
REASON_UNKNOWN_KEY |
unknown_key |
REASON_EXPIRED / REVOKED / SUSPENDED |
expired / revoked / suspended |
REASON_DEVICE_MISMATCH |
device_mismatch |
REASON_STORAGE_ERROR |
storage_error |
REASON_INVALID_RECORD |
invalid_record |
MasterSettingsConfig (ölçüldü):
| Sabit | Değer |
|---|---|
TYPES |
{global:"Global", system:"Sistem", project:"Proje", api:"API", other:"Diğer"} |
CATEGORIES |
{general:"Genel", api:"API Anahtarları", mail:"E-posta Ayarları", seo:"SEO Ayarları", system:"Sistem Ayarları", other:"Diğer"} |
Diğer:
| Sabit | Değer | Kaynak |
|---|---|---|
MasterLicencesService::MAX_KEY_ATTEMPTS |
5 |
:20 (ölçüldü) |
MasterProjectsService::$targetModel |
'master.projects' |
:21 |
| Varsayılan proje sürümü | Version::initial() |
:59, :117 |
| Tier varsayılanı (anahtar üretimi) | FREE |
:136 |
.test/.local soyma (önbellek anahtarı) |
str_replace(['.test','.local'], '', …) |
:268 |
8. Tuzaklar ve kurallar
verify()anahtar DEĞERİNİ hiçbir yere yazmaz.
DatabaseGuardHandlerdocblock'ı (:195): "Anahtar DEĞERİ hiçbir yere
yazılmaz." Yalnızreasongünlüğe yazılır.expires_atbozuk = süresi dolmuş sayılır (isExpired(),:141-150).
strtotime() === falsefail-closed çalışır (Y-1).isAnomalous()rutin kısıtlamadan AYRI tutulur (docblock:57-63):- Rutin = kayıt okundu, alanlar tanınabilir, tarih çözülebiliyor
(PRO/LIFETIME, status≠active, süresi dolmuş) → günlüğe yazılmaz; - Anormal = kayıt yok/boş, alan NULL ya da şemada yok, tarih bozuk →
günlüğe yazılır (B-04, günlük gürültüsü kontrolü).
- Rutin = kayıt okundu, alanlar tanınabilir, tarih çözülebiliyor
blockedReason()tek metottadır (docblock:95-97): "sebep ile karar
AYRI yazılırsa ikisi zamanla ayrışır." — karar ve sebep birlikte döner.MasterLicencesServiceveMasterApplicationsServicetest için açık
repository yuvası sunar (setLicenceRepository(),:26;
setApplicationRepository(),:29). Bu, saf kural sınıfınınnew
yasağından muaftır: DI yuvası, keşif kaydı değildir.issue()verevoke()bu çalışmada ölçülmedi (yazma yolu — master
tablosuna dokunulmadı). İmza ve kural mantığı koddan okundu.MasterApplicationsService::warn()LogThrottlekullanır (:9)
— yani sürüm kapısı reddi günlüğe saatlik bir kez yazılır.
9. Örnek (gerçek koddan)
// Core/Services/Master/LicenceAccessRule.php:89-106
public static function isStatusBlocking(mixed $status): bool
{
return self::blockingReason($status) !== null;
}
public static function blockingReason(mixed $status): ?string
{
return match (trim((string) self::text($status))) {
MasterLicenceConfig::STATUS_REVOKED => MasterLicenceConfig::REASON_REVOKED,
MasterLicenceConfig::STATUS_SUSPENDED => MasterLicenceConfig::REASON_SUSPENDED,
MasterLicenceConfig::STATUS_EXPIRED => MasterLicenceConfig::REASON_EXPIRED,
default => null,
};
}
// Ölçülen çıktı — gerçek verify() çağrısı (sahte repository)
// verify('RBN-TEST-KEY') → valid:true, reason:null
// verify('RBN-TEST-KEY', 'cihash-besik') → valid:false, reason:'device_mismatch'
10. İlgili belgeler
- Core/Services genel · Gatekeepers ·
System · Console - Core/Database/Repositories.md (
masterLicenceRepository,
masterProjectsRepository) · Core/System/Storage.md (BootCacheProvider) ·
Core/System/Kernel.md - Kavram: veritabanı ve kiracılık ·
Kavram: sürümleme ·
Açık sorular §1.2 / §1.7