@@ -560,6 +560,104 @@ protected function getColumnDefinition(string $id): string
560560 return "{$ escapedId } {$ type }" ;
561561 }
562562
563+ /**
564+ * Validate data format against attribute metadata.
565+ *
566+ * @param array<string, mixed> $data
567+ * @throws Exception
568+ */
569+ private function validateDataFormat (array $ data ): void
570+ {
571+ $ attributes = $ this ->getAttributes ();
572+
573+ foreach ($ attributes as $ attribute ) {
574+ /** @var string $attrId */
575+ $ attrId = $ attribute ['$id ' ];
576+ $ required = $ attribute ['required ' ] ?? false ;
577+ $ type = $ attribute ['type ' ] ?? 'string ' ;
578+ /** @var int $size */
579+ $ size = $ attribute ['size ' ] ?? 0 ;
580+
581+ // Check if required attribute is present
582+ if ($ required && !isset ($ data [$ attrId ])) {
583+ throw new Exception ("Required attribute ' {$ attrId }' is missing " );
584+ }
585+
586+ // Skip validation if not present and not required
587+ if (!isset ($ data [$ attrId ])) {
588+ continue ;
589+ }
590+
591+ $ value = $ data [$ attrId ];
592+
593+ // Special handling for tags: accept array (will be JSON-encoded)
594+ if ($ attrId === 'tags ' ) {
595+ if (!is_array ($ value )) {
596+ throw new Exception ("Attribute ' {$ attrId }' must be an array, got " . gettype ($ value ));
597+ }
598+ continue ;
599+ }
600+
601+ // Validate based on attribute type
602+ match ($ type ) {
603+ 'string ' => $ this ->validateStringAttribute ($ attrId , $ value , $ size ),
604+ 'integer ' => $ this ->validateIntegerAttribute ($ attrId , $ value ),
605+ 'datetime ' => $ this ->validateDatetimeAttribute ($ attrId , $ value ),
606+ default => null ,
607+ };
608+ }
609+ }
610+
611+ /**
612+ * Validate string attribute value.
613+ *
614+ * @throws Exception
615+ */
616+ private function validateStringAttribute (string $ attrId , mixed $ value , int $ size ): void
617+ {
618+ if (!is_string ($ value )) {
619+ throw new Exception ("Attribute ' {$ attrId }' must be a string, got " . gettype ($ value ));
620+ }
621+
622+ if ($ size > 0 && strlen ($ value ) > $ size ) {
623+ throw new Exception ("Attribute ' {$ attrId }' exceeds maximum size of {$ size } characters " );
624+ }
625+ }
626+
627+ /**
628+ * Validate integer attribute value.
629+ *
630+ * @throws Exception
631+ */
632+ private function validateIntegerAttribute (string $ attrId , mixed $ value ): void
633+ {
634+ if (!is_int ($ value )) {
635+ throw new Exception ("Attribute ' {$ attrId }' must be an integer, got " . gettype ($ value ));
636+ }
637+ }
638+
639+ /**
640+ * Validate datetime attribute value.
641+ *
642+ * @throws Exception
643+ */
644+ private function validateDatetimeAttribute (string $ attrId , mixed $ value ): void
645+ {
646+ if ($ value instanceof \DateTime) {
647+ return ; // Valid DateTime object
648+ }
649+
650+ if (!is_string ($ value )) {
651+ throw new Exception ("Attribute ' {$ attrId }' must be a DateTime object or string, got " . gettype ($ value ));
652+ }
653+
654+ try {
655+ new \DateTime ($ value );
656+ } catch (\Exception $ e ) {
657+ throw new Exception ("Attribute ' {$ attrId }' is not a valid datetime string: {$ e ->getMessage ()}" );
658+ }
659+ }
660+
563661 /**
564662 * Log a usage metric.
565663 *
@@ -569,10 +667,38 @@ protected function getColumnDefinition(string $id): string
569667 */
570668 public function log (string $ metric , int $ value , string $ period = Usage::PERIOD_1H , array $ tags = []): bool
571669 {
670+ // Validate period
572671 if (!isset (Usage::PERIODS [$ period ])) {
573672 throw new \InvalidArgumentException ('Invalid period. Allowed: ' . implode (', ' , array_keys (Usage::PERIODS )));
574673 }
575674
675+ // Validate metric and value
676+ if (empty ($ metric )) {
677+ throw new Exception ('Metric cannot be empty ' );
678+ }
679+
680+ if (strlen ($ metric ) > 255 ) {
681+ throw new Exception ('Metric exceeds maximum size of 255 characters ' );
682+ }
683+
684+ if ($ value < 0 ) {
685+ throw new Exception ('Value cannot be negative ' );
686+ }
687+
688+ // Validate tags format
689+ if (!is_array ($ tags )) {
690+ throw new Exception ('Tags must be an array ' );
691+ }
692+
693+ // Validate complete data structure
694+ $ data = [
695+ 'metric ' => $ metric ,
696+ 'value ' => $ value ,
697+ 'period ' => $ period ,
698+ 'tags ' => $ tags ,
699+ ];
700+ $ this ->validateDataFormat ($ data );
701+
576702 $ id = uniqid ('' , true );
577703 $ now = new \DateTime ();
578704 $ timestamp = $ this ->formatDateTime ($ now );
@@ -642,6 +768,66 @@ public function logBatch(array $metrics): bool
642768 return true ;
643769 }
644770
771+ // Validate all metrics before processing
772+ foreach ($ metrics as $ index => $ metricData ) {
773+ try {
774+ // Validate required fields exist
775+ if (!isset ($ metricData ['metric ' ])) {
776+ throw new Exception ("Metric # {$ index }: 'metric' is required " );
777+ }
778+ if (!isset ($ metricData ['value ' ])) {
779+ throw new Exception ("Metric # {$ index }: 'value' is required " );
780+ }
781+
782+ $ metric = $ metricData ['metric ' ];
783+ $ value = $ metricData ['value ' ];
784+ $ period = $ metricData ['period ' ] ?? Usage::PERIOD_1H ;
785+
786+ // Validate types
787+ if (!is_string ($ metric )) {
788+ throw new Exception ("Metric # {$ index }: 'metric' must be a string, got " . gettype ($ metric ));
789+ }
790+ if (!is_int ($ value )) {
791+ throw new Exception ("Metric # {$ index }: 'value' must be an integer, got " . gettype ($ value ));
792+ }
793+ if (!is_string ($ period )) {
794+ throw new Exception ("Metric # {$ index }: 'period' must be a string, got " . gettype ($ period ));
795+ }
796+
797+ // Validate metric and value constraints
798+ if (empty ($ metric )) {
799+ throw new Exception ("Metric # {$ index }: 'metric' cannot be empty " );
800+ }
801+ if (strlen ($ metric ) > 255 ) {
802+ throw new Exception ("Metric # {$ index }: 'metric' exceeds maximum size of 255 characters " );
803+ }
804+ if ($ value < 0 ) {
805+ throw new Exception ("Metric # {$ index }: 'value' cannot be negative " );
806+ }
807+
808+ // Validate period
809+ if (!isset (Usage::PERIODS [$ period ])) {
810+ throw new Exception ("Metric # {$ index }: Invalid period ' {$ period }'. Allowed: " . implode (', ' , array_keys (Usage::PERIODS )));
811+ }
812+
813+ // Validate tags if provided
814+ if (isset ($ metricData ['tags ' ]) && !is_array ($ metricData ['tags ' ])) {
815+ throw new Exception ("Metric # {$ index }: 'tags' must be an array, got " . gettype ($ metricData ['tags ' ]));
816+ }
817+
818+ // Validate complete data structure against attributes
819+ $ data = [
820+ 'metric ' => $ metric ,
821+ 'value ' => $ value ,
822+ 'period ' => $ period ,
823+ 'tags ' => $ metricData ['tags ' ] ?? [],
824+ ];
825+ $ this ->validateDataFormat ($ data );
826+ } catch (Exception $ e ) {
827+ throw new Exception ($ e ->getMessage ());
828+ }
829+ }
830+
645831 $ tableName = $ this ->getTableName ();
646832 $ escapedDatabaseAndTable = $ this ->escapeIdentifier ($ this ->database ) . '. ' . $ this ->escapeIdentifier ($ tableName );
647833
@@ -661,18 +847,12 @@ public function logBatch(array $metrics): bool
661847 foreach ($ metrics as $ metricData ) {
662848 $ period = $ metricData ['period ' ] ?? Usage::PERIOD_1H ;
663849
664- if (!isset (Usage::PERIODS [$ period ])) {
665- throw new \InvalidArgumentException ('Invalid period. Allowed: ' . implode (', ' , array_keys (Usage::PERIODS )));
666- }
667-
668850 $ id = uniqid ('' , true );
669851 $ now = new \DateTime ();
670852 $ timestamp = $ this ->formatDateTime ($ now );
671853
672854 $ metric = $ metricData ['metric ' ];
673855 $ value = $ metricData ['value ' ];
674- assert (is_string ($ metric ));
675- assert (is_int ($ value ));
676856
677857 $ valuePlaceholders = [];
678858
0 commit comments