string[], 'cycle' => string[] ] — see graph.js's identically-named function for the full algorithm explanation. */ public static function build_evaluation_order( array $rules ) { $ids = array_map( fn( $r ) => $r['id'], $rules ); $writes_by = []; // fieldId -> [ ruleId => true ] $reads_by = []; // ruleId -> [ fieldId => true ] foreach ( $rules as $rule ) { $reads_by[ $rule['id'] ] = self::collect_read_field_refs( $rule['trigger'] ?? null ); foreach ( array_keys( self::collect_write_field_refs( $rule ) ) as $field_id ) { $writes_by[ $field_id ][ $rule['id'] ] = true; } } $adjacency = array_fill_keys( $ids, [] ); // ruleId -> [ ruleId => true ] it must run before $in_degree = array_fill_keys( $ids, 0 ); foreach ( $rules as $rule ) { foreach ( array_keys( $reads_by[ $rule['id'] ] ) as $field_id ) { foreach ( array_keys( $writes_by[ $field_id ] ?? [] ) as $writer_id ) { if ( $writer_id === $rule['id'] ) { continue; } if ( empty( $adjacency[ $writer_id ][ $rule['id'] ] ) ) { $adjacency[ $writer_id ][ $rule['id'] ] = true; $in_degree[ $rule['id'] ]++; } } } } // Kahn's algorithm. $queue = array_values( array_filter( $ids, fn( $id ) => 0 === $in_degree[ $id ] ) ); $order = []; while ( ! empty( $queue ) ) { $id = array_shift( $queue ); $order[] = $id; foreach ( array_keys( $adjacency[ $id ] ) as $next ) { $in_degree[ $next ]--; if ( 0 === $in_degree[ $next ] ) { $queue[] = $next; } } } $cycle = array_values( array_filter( $ids, fn( $id ) => $in_degree[ $id ] > 0 ) ); return [ 'order' => $order, 'cycle' => $cycle ]; } }